1use std::{fmt::Display, str::FromStr};
2
3use anyhow::{anyhow, Result};
4use cid::Cid;
5use noosphere_core::{
6 authority::{generate_capability, SphereAbility, SPHERE_SEMANTICS},
7 data::{Bundle, Did, Jwt, Link, MemoIpld},
8 error::NoosphereError,
9};
10use noosphere_storage::{base64_decode, base64_encode};
11use reqwest::StatusCode;
12use serde::{Deserialize, Deserializer, Serialize};
13use thiserror::Error;
14use ucan::{
15 chain::ProofChain,
16 crypto::{did::DidParser, KeyMaterial},
17 store::UcanStore,
18 Ucan,
19};
20
21pub trait AsQuery {
22 fn as_query(&self) -> Result<Option<String>>;
23}
24
25impl AsQuery for () {
26 fn as_query(&self) -> Result<Option<String>> {
27 Ok(None)
28 }
29}
30
31fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
34where
35 D: Deserializer<'de>,
36 T: FromStr,
37 T::Err: std::fmt::Display,
38{
39 let opt = Option::<String>::deserialize(de)?;
40 match opt.as_deref() {
41 None | Some("") => Ok(None),
42 Some(s) => FromStr::from_str(s)
43 .map_err(serde::de::Error::custom)
44 .map(Some),
45 }
46}
47
48#[derive(Debug, Serialize, Deserialize)]
50pub struct ReplicateParameters {
51 #[serde(default, deserialize_with = "empty_string_as_none")]
54 pub since: Option<Link<MemoIpld>>,
55}
56
57impl AsQuery for ReplicateParameters {
58 fn as_query(&self) -> Result<Option<String>> {
59 Ok(self.since.as_ref().map(|since| format!("since={since}")))
60 }
61}
62
63#[derive(Debug, Serialize, Deserialize)]
65pub struct FetchParameters {
66 #[serde(default, deserialize_with = "empty_string_as_none")]
69 pub since: Option<Link<MemoIpld>>,
70}
71
72impl AsQuery for FetchParameters {
73 fn as_query(&self) -> Result<Option<String>> {
74 Ok(self.since.as_ref().map(|since| format!("since={since}")))
75 }
76}
77
78#[derive(Debug, Serialize, Deserialize)]
80pub enum FetchResponse {
81 NewChanges {
84 tip: Cid,
87 },
88 UpToDate,
91}
92
93#[derive(Debug, Serialize, Deserialize)]
95pub struct PushBody {
96 pub sphere: Did,
98 pub local_base: Option<Link<MemoIpld>>,
101 pub local_tip: Link<MemoIpld>,
103 pub counterpart_tip: Option<Link<MemoIpld>>,
105 pub blocks: Bundle,
108 pub name_record: Option<Jwt>,
110}
111
112#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
114pub enum PushResponse {
115 Accepted {
117 new_tip: Link<MemoIpld>,
123 blocks: Bundle,
126 },
127 NoChange,
129}
130
131#[derive(Error, Debug)]
132pub enum PushError {
133 #[error("Pushed history conflicts with canonical history")]
134 Conflict,
135 #[error("Missing some implied history")]
136 MissingHistory,
137 #[error("Replica is up to date")]
138 UpToDate,
139 #[error("Internal error")]
140 Internal(anyhow::Error),
141}
142
143impl From<NoosphereError> for PushError {
144 fn from(error: NoosphereError) -> Self {
145 error.into()
146 }
147}
148
149impl From<anyhow::Error> for PushError {
150 fn from(value: anyhow::Error) -> Self {
151 PushError::Internal(value)
152 }
153}
154
155impl From<PushError> for StatusCode {
156 fn from(error: PushError) -> Self {
157 match error {
158 PushError::Conflict => StatusCode::CONFLICT,
159 PushError::MissingHistory => StatusCode::UNPROCESSABLE_ENTITY,
160 PushError::UpToDate => StatusCode::BAD_REQUEST,
161 PushError::Internal(error) => {
162 error!("Internal: {:?}", error);
163 StatusCode::INTERNAL_SERVER_ERROR
164 }
165 }
166 }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct IdentifyResponse {
173 pub gateway_identity: Did,
175 pub sphere_identity: Did,
177 pub signature: String,
179 pub proof: String,
182}
183
184impl IdentifyResponse {
185 pub async fn sign<K>(sphere_identity: &str, key: &K, proof: &Ucan) -> Result<Self>
186 where
187 K: KeyMaterial,
188 {
189 let gateway_identity = Did(key.get_did().await?);
190 let signature = base64_encode(
191 &key.sign(&[gateway_identity.as_bytes(), sphere_identity.as_bytes()].concat())
192 .await?,
193 )?;
194 Ok(IdentifyResponse {
195 gateway_identity,
196 sphere_identity: sphere_identity.into(),
197 signature,
198 proof: proof.encode()?,
199 })
200 }
201
202 pub fn shares_identity_with(&self, other: &IdentifyResponse) -> bool {
203 self.gateway_identity == other.gateway_identity
204 && self.sphere_identity == other.sphere_identity
205 }
206
207 pub async fn verify<S: UcanStore>(&self, did_parser: &mut DidParser, store: &S) -> Result<()> {
219 let gateway_key = did_parser.parse(&self.gateway_identity)?;
220 let payload_bytes = [
221 self.gateway_identity.as_bytes(),
222 self.sphere_identity.as_bytes(),
223 ]
224 .concat();
225 let signature_bytes = base64_decode(&self.signature)?;
226
227 gateway_key.verify(&payload_bytes, &signature_bytes).await?;
229
230 let proof = ProofChain::try_from_token_string(&self.proof, None, did_parser, store).await?;
231
232 if proof.ucan().audience() != self.gateway_identity.as_str() {
233 return Err(anyhow!("Wrong audience!"));
234 }
235
236 let capability = generate_capability(&self.sphere_identity, SphereAbility::Push);
237 let capability_infos = proof.reduce_capabilities(&SPHERE_SEMANTICS);
238
239 for capability_info in capability_infos {
240 if capability_info.capability.enables(&capability)
241 && capability_info
242 .originators
243 .contains(self.sphere_identity.as_str())
244 {
245 return Ok(());
246 }
247 }
248
249 Err(anyhow!("Not authorized!"))
250 }
251}
252
253impl Display for IdentifyResponse {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 write!(
256 f,
257 "((Gateway {}), (Sphere {}))",
258 self.gateway_identity, self.sphere_identity
259 )
260 }
261}