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}