Alternative ATProto PDS implementation
1//! PLC operations. 2use std::collections::HashMap; 3 4use anyhow::{Context as _, bail}; 5use base64::Engine as _; 6use serde::{Deserialize, Serialize}; 7use tracing::debug; 8 9use crate::{Client, RotationKey}; 10 11/// The URL of the public PLC directory. 12const PLC_DIRECTORY: &str = "https://plc.directory/"; 13 14#[derive(Debug, Deserialize, Serialize, Clone)] 15#[serde(rename_all = "camelCase", tag = "type")] 16/// A PLC service. 17pub(crate) enum PlcService { 18 #[serde(rename = "AtprotoPersonalDataServer")] 19 /// A personal data server. 20 Pds { 21 /// The URL of the PDS. 22 endpoint: String, 23 }, 24} 25 26#[expect( 27 clippy::arbitrary_source_item_ordering, 28 reason = "serialized data might be structured" 29)] 30#[derive(Debug, Deserialize, Serialize, Clone)] 31#[serde(rename_all = "camelCase")] 32pub(crate) struct PlcOperation { 33 #[serde(rename = "type")] 34 pub typ: String, 35 pub rotation_keys: Vec<String>, 36 pub verification_methods: HashMap<String, String>, 37 pub also_known_as: Vec<String>, 38 pub services: HashMap<String, PlcService>, 39 pub prev: Option<String>, 40} 41 42impl PlcOperation { 43 /// Sign an operation with the provided signature. 44 pub(crate) fn sign(self, sig: Vec<u8>) -> SignedPlcOperation { 45 SignedPlcOperation { 46 typ: self.typ, 47 rotation_keys: self.rotation_keys, 48 verification_methods: self.verification_methods, 49 also_known_as: self.also_known_as, 50 services: self.services, 51 prev: self.prev, 52 sig: base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(sig), 53 } 54 } 55} 56 57#[expect( 58 clippy::arbitrary_source_item_ordering, 59 reason = "serialized data might be structured" 60)] 61#[derive(Debug, Deserialize, Serialize, Clone)] 62#[serde(rename_all = "camelCase")] 63/// A signed PLC operation. 64pub(crate) struct SignedPlcOperation { 65 #[serde(rename = "type")] 66 pub typ: String, 67 pub rotation_keys: Vec<String>, 68 pub verification_methods: HashMap<String, String>, 69 pub also_known_as: Vec<String>, 70 pub services: HashMap<String, PlcService>, 71 pub prev: Option<String>, 72 pub sig: String, 73} 74 75pub(crate) fn sign_op(rkey: &RotationKey, op: PlcOperation) -> anyhow::Result<SignedPlcOperation> { 76 let bytes = serde_ipld_dagcbor::to_vec(&op).context("failed to encode op")?; 77 let bytes = rkey.sign(&bytes).context("failed to sign op")?; 78 79 Ok(op.sign(bytes)) 80} 81 82/// Submit a PLC operation to the public directory. 83pub(crate) async fn submit( 84 client: &Client, 85 did: &str, 86 op: &SignedPlcOperation, 87) -> anyhow::Result<()> { 88 debug!( 89 "submitting {} {}", 90 did, 91 serde_json::to_string(&op).context("should serialize")? 92 ); 93 94 let res = client 95 .post(format!("{PLC_DIRECTORY}{did}")) 96 .json(&op) 97 .send() 98 .await 99 .context("failed to send directory request")?; 100 101 if res.status().is_success() { 102 Ok(()) 103 } else { 104 let e = res 105 .json::<serde_json::Value>() 106 .await 107 .context("failed to read error response")?; 108 109 bail!( 110 "error from PLC directory: {}", 111 serde_json::to_string(&e).context("should serialize")? 112 ); 113 } 114}