noosphere_core/api/v0alpha1/
data.rs1use std::fmt::Display;
2
3use crate::api::{
4 data::{empty_string_as_none, AsQuery},
5 StatusCode,
6};
7use crate::{
8 authority::{generate_capability, SphereAbility, SPHERE_SEMANTICS},
9 data::{Bundle, Did, Jwt, Link, MemoIpld},
10 error::NoosphereError,
11};
12use anyhow::{anyhow, Result};
13use cid::Cid;
14use noosphere_storage::{base64_decode, base64_encode};
15use serde::{Deserialize, Serialize};
16use thiserror::Error;
17use ucan::{
18 chain::ProofChain,
19 crypto::{did::DidParser, KeyMaterial},
20 store::UcanStore,
21 Ucan,
22};
23
24#[derive(Debug, Serialize, Deserialize)]
26pub struct ReplicateParameters {
27 #[serde(default, deserialize_with = "empty_string_as_none")]
30 pub since: Option<Link<MemoIpld>>,
31
32 #[serde(default)]
38 pub include_content: bool,
39}
40
41impl AsQuery for ReplicateParameters {
42 fn as_query(&self) -> Result<Option<String>> {
43 let mut params = Vec::new();
44 if let Some(since) = self.since {
45 params.push(format!("since={since}"));
46 }
47 if self.include_content {
48 params.push(String::from("include_content=true"))
49 }
50
51 let query = if !params.is_empty() {
52 Some(params.join("&"))
53 } else {
54 None
55 };
56
57 Ok(query)
58 }
59}
60
61#[derive(Clone)]
63pub enum ReplicationMode {
64 Cid(Cid),
66 Did(Did),
69}
70
71impl From<Cid> for ReplicationMode {
72 fn from(value: Cid) -> Self {
73 ReplicationMode::Cid(value)
74 }
75}
76
77impl From<Did> for ReplicationMode {
78 fn from(value: Did) -> Self {
79 ReplicationMode::Did(value)
80 }
81}
82
83impl Display for ReplicationMode {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 ReplicationMode::Cid(cid) => Display::fmt(cid, f),
87 ReplicationMode::Did(did) => Display::fmt(did, f),
88 }
89 }
90}
91
92#[derive(Debug, Serialize, Deserialize)]
94pub struct FetchParameters {
95 #[serde(default, deserialize_with = "empty_string_as_none")]
98 pub since: Option<Link<MemoIpld>>,
99}
100
101impl AsQuery for FetchParameters {
102 fn as_query(&self) -> Result<Option<String>> {
103 Ok(self.since.as_ref().map(|since| format!("since={since}")))
104 }
105}
106
107#[derive(Debug, Serialize, Deserialize)]
109pub enum FetchResponse {
110 NewChanges {
113 tip: Cid,
116 },
117 UpToDate,
120}
121
122#[derive(Debug, Serialize, Deserialize)]
124pub struct PushBody {
125 pub sphere: Did,
127 pub local_base: Option<Link<MemoIpld>>,
130 pub local_tip: Link<MemoIpld>,
132 pub counterpart_tip: Option<Link<MemoIpld>>,
134 pub blocks: Bundle,
137 pub name_record: Option<Jwt>,
139}
140
141#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
143pub enum PushResponse {
144 Accepted {
146 new_tip: Link<MemoIpld>,
152 blocks: Bundle,
155 },
156 NoChange,
158}
159
160#[derive(Error, Debug)]
162pub enum PushError {
163 #[allow(missing_docs)]
164 #[error("Pushed history conflicts with canonical history")]
165 Conflict,
166 #[allow(missing_docs)]
167 #[error("Missing some implied history")]
168 MissingHistory,
169 #[allow(missing_docs)]
170 #[error("Replica is up to date")]
171 UpToDate,
172 #[allow(missing_docs)]
173 #[error("Internal error")]
174 Internal(anyhow::Error),
175}
176
177impl From<NoosphereError> for PushError {
178 fn from(error: NoosphereError) -> Self {
179 error.into()
180 }
181}
182
183impl From<anyhow::Error> for PushError {
184 fn from(value: anyhow::Error) -> Self {
185 PushError::Internal(value)
186 }
187}
188
189impl From<PushError> for StatusCode {
190 fn from(error: PushError) -> Self {
191 match error {
192 PushError::Conflict => StatusCode::CONFLICT,
193 PushError::MissingHistory => StatusCode::UNPROCESSABLE_ENTITY,
194 PushError::UpToDate => StatusCode::BAD_REQUEST,
195 PushError::Internal(error) => {
196 error!("Internal: {:?}", error);
197 StatusCode::INTERNAL_SERVER_ERROR
198 }
199 }
200 }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct IdentifyResponse {
207 pub gateway_identity: Did,
209 pub sphere_identity: Did,
211 pub signature: String,
213 pub proof: String,
216}
217
218impl IdentifyResponse {
219 pub async fn sign<K>(sphere_identity: &str, key: &K, proof: &Ucan) -> Result<Self>
221 where
222 K: KeyMaterial,
223 {
224 let gateway_identity = Did(key.get_did().await?);
225 let signature = base64_encode(
226 &key.sign(&[gateway_identity.as_bytes(), sphere_identity.as_bytes()].concat())
227 .await?,
228 )?;
229 Ok(IdentifyResponse {
230 gateway_identity,
231 sphere_identity: sphere_identity.into(),
232 signature,
233 proof: proof.encode()?,
234 })
235 }
236
237 pub fn shares_identity_with(&self, other: &IdentifyResponse) -> bool {
240 self.gateway_identity == other.gateway_identity
241 && self.sphere_identity == other.sphere_identity
242 }
243
244 pub async fn verify<S: UcanStore>(&self, did_parser: &mut DidParser, store: &S) -> Result<()> {
256 let gateway_key = did_parser.parse(&self.gateway_identity)?;
257 let payload_bytes = [
258 self.gateway_identity.as_bytes(),
259 self.sphere_identity.as_bytes(),
260 ]
261 .concat();
262 let signature_bytes = base64_decode(&self.signature)?;
263
264 gateway_key.verify(&payload_bytes, &signature_bytes).await?;
266
267 let proof = ProofChain::try_from_token_string(&self.proof, None, did_parser, store).await?;
268
269 if proof.ucan().audience() != self.gateway_identity.as_str() {
270 return Err(anyhow!("Wrong audience!"));
271 }
272
273 let capability = generate_capability(&self.sphere_identity, SphereAbility::Push);
274 let capability_infos = proof.reduce_capabilities(&SPHERE_SEMANTICS);
275
276 for capability_info in capability_infos {
277 if capability_info.capability.enables(&capability)
278 && capability_info
279 .originators
280 .contains(self.sphere_identity.as_str())
281 {
282 return Ok(());
283 }
284 }
285
286 Err(anyhow!("Not authorized!"))
287 }
288}
289
290impl Display for IdentifyResponse {
291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292 write!(
293 f,
294 "((Gateway {}), (Sphere {}))",
295 self.gateway_identity, self.sphere_identity
296 )
297 }
298}