aboutsummaryrefslogtreecommitdiffstats
path: root/adenosine
diff options
context:
space:
mode:
Diffstat (limited to 'adenosine')
-rw-r--r--adenosine/Cargo.toml37
-rw-r--r--adenosine/src/app_bsky/mod.rs165
-rw-r--r--adenosine/src/auth.rs37
-rw-r--r--adenosine/src/com_atproto/mod.rs29
-rw-r--r--adenosine/src/com_atproto/repo/mod.rs53
-rw-r--r--adenosine/src/ipld.rs47
-rw-r--r--adenosine/src/lib.rs231
-rw-r--r--adenosine/src/plc.rs (renamed from adenosine/src/did.rs)0
-rw-r--r--adenosine/src/xrpc.rs186
-rw-r--r--adenosine/tests/bigger.carbin0 -> 60050 bytes
-rw-r--r--adenosine/tests/example_repo.carbin0 -> 7390 bytes
-rw-r--r--adenosine/tests/test_mst_interop.rs152
-rw-r--r--adenosine/tests/test_repro_mst.rs26
13 files changed, 721 insertions, 242 deletions
diff --git a/adenosine/Cargo.toml b/adenosine/Cargo.toml
index 456b0fe..f4705ae 100644
--- a/adenosine/Cargo.toml
+++ b/adenosine/Cargo.toml
@@ -15,33 +15,32 @@ repository = "https://gitlab.com/bnewbold/adenosine"
[dependencies]
# NOTE: could try 'rustls-tls' feature instead of default native TLS?
-reqwest = { version = "0.11", features = ["blocking", "json"] }
-serde = { version = "1", features = ["serde_derive"] }
-serde_json = "1"
+anyhow = "1"
+async-trait = "0.1"
base64 = "0.13"
-regex = "1"
-lazy_static = "1"
+bs58 = "0.4"
data-encoding = "2"
+futures = "0.3"
+ipfs-sqlite-block-store = "0.13"
+lazy_static = "1"
+libipld = { version = "0.14", features = ["dag-cbor", "derive"] }
+log = "0.4"
+multibase = "0.9"
rand = "0.8"
+regex = "1"
+reqwest = { version = "0.11", features = ["blocking", "json"] }
+serde = { version = "1", features = ["serde_derive"] }
+serde_json = "1"
+sha256 = "1"
time = { version = "0.3", features = ["formatting"] }
+tokio = { version = "1", features = ["full"] }
+
+# crypto/auth stuff
k256 = { version = "0.11", features = ["ecdsa"] }
p256 = { version = "0.11", features = ["ecdsa"] }
-bs58 = "0.4"
-libipld = "0.14"
-ipfs-sqlite-block-store = "0.13"
-tokio = { version = "1", features = ["full"] }
-futures = "0.3"
-sha256 = "1"
-multibase = "0.9"
ucan = "0.7.0-alpha.1"
-async-trait = "0.1"
# for vendored iroh-car
-thiserror = "1.0"
integer-encoding = { version = "3", features = ["tokio_async"] }
multihash = "0.16"
-
-# uncertain about these...
-anyhow = "1"
-log = "0.4"
-env_logger = "0.7"
+thiserror = "1.0"
diff --git a/adenosine/src/app_bsky/mod.rs b/adenosine/src/app_bsky/mod.rs
new file mode 100644
index 0000000..18a0449
--- /dev/null
+++ b/adenosine/src/app_bsky/mod.rs
@@ -0,0 +1,165 @@
+/// app.bsky types (manually entered)
+use serde_json::Value;
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Subject {
+ pub uri: String,
+ // TODO: CID is required
+ pub cid: Option<String>,
+}
+
+/// Generic over Re-post and Like
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct RefRecord {
+ pub subject: Subject,
+ pub createdAt: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct FollowSubject {
+ pub did: String,
+ // pub declarationCid: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct FollowRecord {
+ pub subject: FollowSubject,
+ pub createdAt: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct ProfileRecord {
+ pub displayName: String,
+ pub description: Option<String>,
+}
+
+// app.bsky.system.actorUser or app.bsky.system.actorScene
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Declaration {
+ pub actorType: String,
+}
+
+// actorType: app.bsky.system.actorUser
+// cid: bafyreid27zk7lbis4zw5fz4podbvbs4fc5ivwji3dmrwa6zggnj4bnd57u
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct DeclRef {
+ pub actorType: String,
+ pub cid: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Profile {
+ pub did: String,
+ pub declaration: DeclRef,
+ pub handle: String,
+ // for simple accounts, 'creator' is just the did
+ pub creator: String,
+ pub displayName: Option<String>,
+ pub description: Option<String>,
+ pub followersCount: u64,
+ pub followsCount: u64,
+ pub membersCount: u64,
+ pub postsCount: u64,
+ pub myState: serde_json::Value,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct GenericFeed {
+ pub feed: Vec<FeedItem>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct User {
+ pub did: String,
+ pub handle: String,
+ pub displayName: Option<String>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct FeedItem {
+ pub uri: String,
+ pub cid: String,
+ pub author: User,
+ pub repostedBy: Option<User>,
+ pub record: Value,
+ //pub embed?: RecordEmbed | ExternalEmbed | UnknownEmbed,
+ pub embed: Option<Value>,
+ pub replyCount: u64,
+ pub repostCount: u64,
+ pub upvoteCount: u64,
+ pub downvoteCount: u64,
+ pub indexedAt: String,
+ pub myState: Option<Value>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Post {
+ pub text: String,
+ pub reply: Option<PostReply>,
+ pub createdAt: Option<String>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct PostReply {
+ pub parent: Subject,
+ pub root: Subject,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct PostThread {
+ pub thread: ThreadItem,
+}
+
+// TODO: 'parent' and 'replies' should allow "NotFoundPost" for references that point to an unknown
+// URI
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct ThreadItem {
+ pub uri: String,
+ pub cid: String,
+ pub author: User,
+ pub record: Value,
+ //pub embed?: RecordEmbed | ExternalEmbed | UnknownEmbed,
+ pub embed: Option<Value>,
+ pub parent: Option<Box<ThreadItem>>,
+ pub replyCount: u64,
+ pub replies: Option<Vec<ThreadItem>>,
+ pub repostCount: u64,
+ pub upvoteCount: u64,
+ pub downvoteCount: u64,
+ pub indexedAt: String,
+ pub myState: Option<Value>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct FollowTarget {
+ // TODO: nested follow list?
+ pub subject: Subject,
+ pub did: String,
+ pub handle: String,
+ pub displayName: Option<String>,
+ pub createdAt: Option<String>,
+ pub indexedAt: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Follow {
+ // TODO: nested follow list?
+ pub subject: Subject,
+ pub follows: FollowTarget,
+}
diff --git a/adenosine/src/auth.rs b/adenosine/src/auth.rs
new file mode 100644
index 0000000..ff931ef
--- /dev/null
+++ b/adenosine/src/auth.rs
@@ -0,0 +1,37 @@
+use anyhow::anyhow;
+pub use anyhow::Result;
+use serde_json::Value;
+
+/// Tries to parse a DID internal identifier from a JWT (as base64-encoded token)
+pub fn parse_did_from_jwt(jwt: &str) -> Result<String> {
+ let second_b64 = jwt.split('.').nth(1).ok_or(anyhow!("couldn't parse JWT"))?;
+ let second_json: Vec<u8> = base64::decode_config(second_b64, base64::URL_SAFE)?;
+ let obj: Value = serde_json::from_slice(&second_json)?;
+ // trying to also support pulling "aud" as DID; not sure this is actually correct use of
+ // UCAN/JWT semantics?
+ let did = obj["sub"]
+ .as_str()
+ .or(obj["aud"].as_str())
+ .ok_or(anyhow!("couldn't find DID subject in JWT"))?
+ .to_string();
+ if !did.starts_with("did:") {
+ return Err(anyhow!("couldn't find DID subject in JWT"));
+ }
+ Ok(did)
+}
+
+#[test]
+fn test_parse_jwt() {
+ assert!(parse_did_from_jwt(".").is_err());
+ // JWT from atproto ("sub")
+ assert_eq!(
+ parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6cGxjOmV4M3NpNTI3Y2QyYW9nYnZpZGtvb296YyIsImlhdCI6MTY2NjgyOTM5M30.UvZgTqvaJICONa1wIUT1bny7u3hqVAqWhWy3qeuyZrE").unwrap(),
+ "did:plc:ex3si527cd2aogbvidkooozc",
+ );
+ // UCAN from adenosine-pds ("aud")
+ assert_eq!(
+ parse_did_from_jwt("eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6cGxjOnM3b25ieWphN2MzeXJzZ3Zob2xrbHM1YiIsImV4cCI6MTY3NTM4Mzg2NywiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6RG5hZWRHVGJkb0Frb1NlOG96a3k1WHAzMjZTVFpUSm50aDlHY2dxaTZQYjNzYjczIiwibm5jIjoiTnZURDhENWZjNXFpalIyMWJ1V2Z1ZE02dzlBM2drSy1ac3RtUW03b21pdyIsInByZiI6W119.QwZkb9R17tNhXnY_roqFYgdiIgUnSC18FYWQb3PcH6BU1R5l4W_T4XdACyczPGfM-jAnF2r2loBXDntYVS6N5A").unwrap(),
+ "did:plc:s7onbyja7c3yrsgvholkls5b",
+ );
+ assert!(parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9").is_err());
+}
diff --git a/adenosine/src/com_atproto/mod.rs b/adenosine/src/com_atproto/mod.rs
new file mode 100644
index 0000000..8e2317a
--- /dev/null
+++ b/adenosine/src/com_atproto/mod.rs
@@ -0,0 +1,29 @@
+// com.atproto types (manually entered)
+
+pub mod repo;
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
+pub struct AccountRequest {
+ pub email: String,
+ pub handle: String,
+ pub password: String,
+ pub inviteCode: Option<String>,
+ pub recoveryKey: Option<String>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
+pub struct SessionRequest {
+ pub handle: String,
+ pub password: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Session {
+ pub did: String,
+ pub name: String,
+ pub accessJwt: String,
+ pub refreshJwt: String,
+}
diff --git a/adenosine/src/com_atproto/repo/mod.rs b/adenosine/src/com_atproto/repo/mod.rs
new file mode 100644
index 0000000..aa66e98
--- /dev/null
+++ b/adenosine/src/com_atproto/repo/mod.rs
@@ -0,0 +1,53 @@
+/// com.atproto.repo types (manually entered)
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Describe {
+ pub name: String,
+ pub did: String,
+ pub didDoc: serde_json::Value,
+ pub collections: Vec<String>,
+ pub nameIsCorrect: bool,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct CreateRecord {
+ pub did: String,
+ pub collection: String,
+ pub record: serde_json::Value,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct PutRecord {
+ pub did: String,
+ pub collection: String,
+ pub rkey: String,
+ pub record: serde_json::Value,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct DeleteRecord {
+ pub did: String,
+ pub collection: String,
+ pub rkey: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct BatchWriteBody {
+ pub did: String,
+ pub writes: Vec<BatchWrite>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct BatchWrite {
+ #[serde(rename = "type")]
+ pub op_type: String,
+ pub collection: String,
+ pub rkey: Option<String>,
+ pub value: serde_json::Value,
+}
diff --git a/adenosine/src/ipld.rs b/adenosine/src/ipld.rs
new file mode 100644
index 0000000..65a56e4
--- /dev/null
+++ b/adenosine/src/ipld.rs
@@ -0,0 +1,47 @@
+use libipld::{Cid, Ipld};
+use serde_json::{json, Value};
+use std::collections::BTreeMap;
+use std::str::FromStr;
+
+/// Intentionally serializing with this instead of DAG-JSON, because ATP schemas don't encode CID
+/// links in any special way, they just pass the CID as a string.
+pub fn ipld_into_json_value(val: Ipld) -> Value {
+ match val {
+ Ipld::Null => Value::Null,
+ Ipld::Bool(b) => Value::Bool(b),
+ Ipld::Integer(v) => json!(v),
+ Ipld::Float(v) => json!(v),
+ Ipld::String(s) => Value::String(s),
+ Ipld::Bytes(b) => Value::String(data_encoding::BASE64_NOPAD.encode(&b)),
+ Ipld::List(l) => Value::Array(l.into_iter().map(ipld_into_json_value).collect()),
+ Ipld::Map(m) => Value::Object(serde_json::Map::from_iter(
+ m.into_iter().map(|(k, v)| (k, ipld_into_json_value(v))),
+ )),
+ Ipld::Link(c) => Value::String(c.to_string()),
+ }
+}
+
+/// Crude reverse generation
+///
+/// Does not handle base64 to bytes, and the link generation is pretty simple (object elements with
+/// key "car"). Numbers always come through as f64 (float).
+pub fn json_value_into_ipld(val: Value) -> Ipld {
+ match val {
+ Value::Null => Ipld::Null,
+ Value::Bool(b) => Ipld::Bool(b),
+ Value::String(s) => Ipld::String(s),
+ // TODO: handle numbers better?
+ Value::Number(v) => Ipld::Float(v.as_f64().unwrap()),
+ Value::Array(l) => Ipld::List(l.into_iter().map(json_value_into_ipld).collect()),
+ Value::Object(m) => {
+ let map: BTreeMap<String, Ipld> = BTreeMap::from_iter(m.into_iter().map(|(k, v)| {
+ if k == "car" && v.is_string() {
+ (k, Ipld::Link(Cid::from_str(v.as_str().unwrap()).unwrap()))
+ } else {
+ (k, json_value_into_ipld(v))
+ }
+ }));
+ Ipld::Map(map)
+ }
+ }
+}
diff --git a/adenosine/src/lib.rs b/adenosine/src/lib.rs
index dc4a1b9..49701a1 100644
--- a/adenosine/src/lib.rs
+++ b/adenosine/src/lib.rs
@@ -1,232 +1,17 @@
-use anyhow::anyhow;
-pub use anyhow::Result;
-use reqwest::header;
-use serde_json::Value;
-use std::collections::HashMap;
-use std::str::FromStr;
-use std::time::Duration;
-
+pub mod app_bsky;
+pub mod auth;
pub mod car;
+pub mod com_atproto;
pub mod crypto;
-pub mod did;
pub mod identifiers;
+pub mod ipld;
pub mod mst;
+pub mod plc;
pub mod repo;
-pub mod ucan_p256;
-pub mod vendored;
-use identifiers::Nsid;
-
-static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub enum XrpcMethod {
- Get,
- Post,
-}
-
-impl FromStr for XrpcMethod {
- type Err = anyhow::Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- match s {
- "get" => Ok(XrpcMethod::Get),
- "post" => Ok(XrpcMethod::Post),
- _ => Err(anyhow!("unknown method: {}", s)),
- }
- }
-}
-
-pub struct XrpcClient {
- http_client: reqwest::blocking::Client,
- host: String,
-}
-
-impl XrpcClient {
- pub fn new(host: String, auth_token: Option<String>) -> Result<Self> {
- let mut headers = header::HeaderMap::new();
- if let Some(token) = &auth_token {
- let mut auth_value = header::HeaderValue::from_str(&format!("Bearer {}", token))?;
- auth_value.set_sensitive(true);
- headers.insert(header::AUTHORIZATION, auth_value);
- };
-
- let http_client = reqwest::blocking::Client::builder()
- .default_headers(headers)
- .user_agent(APP_USER_AGENT)
- .timeout(Duration::from_secs(30))
- //.danger_accept_invalid_certs(true)
- .build()
- .expect("ERROR :: Could not build reqwest client");
+pub mod xrpc;
- Ok(XrpcClient { http_client, host })
- }
-
- pub fn get(
- &self,
- nsid: &Nsid,
- params: Option<HashMap<String, String>>,
- ) -> Result<Option<Value>> {
- log::debug!("XRPC GET endpoint={} params={:?}", nsid, params);
- let params: HashMap<String, String> = params.unwrap_or_default();
- let res = self
- .http_client
- .get(format!("{}/xrpc/{}", self.host, nsid))
- .query(&params)
- .send()?;
- // TODO: refactor this error handling stuff into single method
- if res.status() == 400 {
- let val: Value = res.json()?;
- return Err(anyhow!(
- "XRPC Bad Request (400): {}",
- val["message"].as_str().unwrap_or("unknown")
- ));
- } else if res.status() == 500 {
- let val: Value = res.json()?;
- return Err(anyhow!(
- "XRPC Internal Error (500): {}",
- val["message"].as_str().unwrap_or("unknown")
- ));
- }
- let res = res.error_for_status()?;
- Ok(res.json()?)
- }
-
- pub fn get_to_writer<W: std::io::Write>(
- &self,
- nsid: &Nsid,
- params: Option<HashMap<String, String>>,
- output: &mut W,
- ) -> Result<u64> {
- let params: HashMap<String, String> = params.unwrap_or_default();
- let res = self
- .http_client
- .get(format!("{}/xrpc/{}", self.host, nsid))
- .query(&params)
- .send()?;
- if res.status() == 400 {
- let val: Value = res.json()?;
- return Err(anyhow!(
- "XRPC Bad Request (400): {}",
- val["message"].as_str().unwrap_or("unknown")
- ));
- } else if res.status() == 500 {
- let val: Value = res.json()?;
- return Err(anyhow!(
- "XRPC Internal Error (500): {}",
- val["message"].as_str().unwrap_or("unknown")
- ));
- }
- let mut res = res.error_for_status()?;
- Ok(res.copy_to(output)?)
- }
-
- pub fn post(
- &self,
- nsid: &Nsid,
- params: Option<HashMap<String, String>>,
- body: Option<Value>,
- ) -> Result<Option<Value>> {
- let params: HashMap<String, String> = params.unwrap_or_default();
- log::debug!(
- "XRPC POST endpoint={} params={:?} body={:?}",
- nsid,
- params,
- body
- );
- let mut req = self
- .http_client
- .post(format!("{}/xrpc/{}", self.host, nsid))
- .query(&params);
- req = if let Some(b) = body {
- req.json(&b)
- } else {
- req
- };
- let res = req.send()?;
- if res.status() == 400 {
- let val: Value = res.json()?;
- return Err(anyhow!(
- "XRPC Bad Request (400): {}",
- val["message"].as_str().unwrap_or("unknown")
- ));
- } else if res.status() == 500 {
- let val: Value = res.json()?;
- return Err(anyhow!(
- "XRPC Internal Error (500): {}",
- val["message"].as_str().unwrap_or("unknown")
- ));
- }
- let res = res.error_for_status()?;
- if res.content_length() == Some(0) {
- Ok(None)
- } else {
- Ok(res.json()?)
- }
- }
-
- pub fn post_cbor_from_reader<R: std::io::Read>(
- &self,
- nsid: &Nsid,
- params: Option<HashMap<String, String>>,
- input: &mut R,
- ) -> Result<Option<Value>> {
- let params: HashMap<String, String> = params.unwrap_or_default();
- let mut buf: Vec<u8> = Vec::new();
- input.read_to_end(&mut buf)?;
- let res = self
- .http_client
- .post(format!("{}/xrpc/{}", self.host, nsid))
- .query(&params)
- .header(reqwest::header::CONTENT_TYPE, "application/cbor")
- .body(buf)
- .send()?;
- if res.status() == 400 {
- let val: Value = res.json()?;
- return Err(anyhow!(
- "XRPC Bad Request: {}",
- val["message"].as_str().unwrap_or("unknown")
- ));
- }
- let res = res.error_for_status()?;
- Ok(res.json()?)
- }
-
- // reqwest::blocking::Body
-}
-
-/// Tries to parse a DID internal identifier from a JWT (as base64-encoded token)
-pub fn parse_did_from_jwt(jwt: &str) -> Result<String> {
- let second_b64 = jwt.split('.').nth(1).ok_or(anyhow!("couldn't parse JWT"))?;
- let second_json: Vec<u8> = base64::decode_config(second_b64, base64::URL_SAFE)?;
- let obj: Value = serde_json::from_slice(&second_json)?;
- // trying to also support pulling "aud" as DID; not sure this is actually correct use of
- // UCAN/JWT semantics?
- let did = obj["sub"]
- .as_str()
- .or(obj["aud"].as_str())
- .ok_or(anyhow!("couldn't find DID subject in JWT"))?
- .to_string();
- if !did.starts_with("did:") {
- return Err(anyhow!("couldn't find DID subject in JWT"));
- }
- Ok(did)
-}
-
-#[test]
-fn test_parse_jwt() {
- assert!(parse_did_from_jwt(".").is_err());
- // JWT from atproto ("sub")
- assert_eq!(
- parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6cGxjOmV4M3NpNTI3Y2QyYW9nYnZpZGtvb296YyIsImlhdCI6MTY2NjgyOTM5M30.UvZgTqvaJICONa1wIUT1bny7u3hqVAqWhWy3qeuyZrE").unwrap(),
- "did:plc:ex3si527cd2aogbvidkooozc",
- );
- // UCAN from adenosine-pds ("aud")
- assert_eq!(
- parse_did_from_jwt("eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6cGxjOnM3b25ieWphN2MzeXJzZ3Zob2xrbHM1YiIsImV4cCI6MTY3NTM4Mzg2NywiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6RG5hZWRHVGJkb0Frb1NlOG96a3k1WHAzMjZTVFpUSm50aDlHY2dxaTZQYjNzYjczIiwibm5jIjoiTnZURDhENWZjNXFpalIyMWJ1V2Z1ZE02dzlBM2drSy1ac3RtUW03b21pdyIsInByZiI6W119.QwZkb9R17tNhXnY_roqFYgdiIgUnSC18FYWQb3PcH6BU1R5l4W_T4XdACyczPGfM-jAnF2r2loBXDntYVS6N5A").unwrap(),
- "did:plc:s7onbyja7c3yrsgvholkls5b",
- );
- assert!(parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9").is_err());
-}
+mod ucan_p256;
+mod vendored;
/// Helper to generate the current timestamp as right now, UTC, formatted as an RFC 3339 string.
///
diff --git a/adenosine/src/did.rs b/adenosine/src/plc.rs
index c7d7d10..c7d7d10 100644
--- a/adenosine/src/did.rs
+++ b/adenosine/src/plc.rs
diff --git a/adenosine/src/xrpc.rs b/adenosine/src/xrpc.rs
new file mode 100644
index 0000000..382c7fb
--- /dev/null
+++ b/adenosine/src/xrpc.rs
@@ -0,0 +1,186 @@
+use crate::identifiers::Nsid;
+use anyhow::anyhow;
+pub use anyhow::Result;
+use reqwest::header;
+use serde_json::Value;
+use std::collections::HashMap;
+use std::str::FromStr;
+use std::time::Duration;
+
+static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum XrpcMethod {
+ Get,
+ Post,
+}
+
+impl FromStr for XrpcMethod {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "get" => Ok(XrpcMethod::Get),
+ "post" => Ok(XrpcMethod::Post),
+ _ => Err(anyhow!("unknown method: {}", s)),
+ }
+ }
+}
+
+pub struct XrpcClient {
+ http_client: reqwest::blocking::Client,
+ host: String,
+}
+
+impl XrpcClient {
+ pub fn new(host: String, auth_token: Option<String>) -> Result<Self> {
+ let mut headers = header::HeaderMap::new();
+ if let Some(token) = &auth_token {
+ let mut auth_value = header::HeaderValue::from_str(&format!("Bearer {}", token))?;
+ auth_value.set_sensitive(true);
+ headers.insert(header::AUTHORIZATION, auth_value);
+ };
+
+ let http_client = reqwest::blocking::Client::builder()
+ .default_headers(headers)
+ .user_agent(APP_USER_AGENT)
+ .timeout(Duration::from_secs(30))
+ //.danger_accept_invalid_certs(true)
+ .build()
+ .expect("ERROR :: Could not build reqwest client");
+
+ Ok(XrpcClient { http_client, host })
+ }
+
+ pub fn get(
+ &self,
+ nsid: &Nsid,
+ params: Option<HashMap<String, String>>,
+ ) -> Result<Option<Value>> {
+ log::debug!("XRPC GET endpoint={} params={:?}", nsid, params);
+ let params: HashMap<String, String> = params.unwrap_or_default();
+ let res = self
+ .http_client
+ .get(format!("{}/xrpc/{}", self.host, nsid))
+ .query(&params)
+ .send()?;
+ // TODO: refactor this error handling stuff into single method
+ if res.status() == 400 {
+ let val: Value = res.json()?;
+ return Err(anyhow!(
+ "XRPC Bad Request (400): {}",
+ val["message"].as_str().unwrap_or("unknown")
+ ));
+ } else if res.status() == 500 {
+ let val: Value = res.json()?;
+ return Err(anyhow!(
+ "XRPC Internal Error (500): {}",
+ val["message"].as_str().unwrap_or("unknown")
+ ));
+ }
+ let res = res.error_for_status()?;
+ Ok(res.json()?)
+ }
+
+ pub fn get_to_writer<W: std::io::Write>(
+ &self,
+ nsid: &Nsid,
+ params: Option<HashMap<String, String>>,
+ output: &mut W,
+ ) -> Result<u64> {
+ let params: HashMap<String, String> = params.unwrap_or_default();
+ let res = self
+ .http_client
+ .get(format!("{}/xrpc/{}", self.host, nsid))
+ .query(&params)
+ .send()?;
+ if res.status() == 400 {
+ let val: Value = res.json()?;
+ return Err(anyhow!(
+ "XRPC Bad Request (400): {}",
+ val["message"].as_str().unwrap_or("unknown")
+ ));
+ } else if res.status() == 500 {
+ let val: Value = res.json()?;
+ return Err(anyhow!(
+ "XRPC Internal Error (500): {}",
+ val["message"].as_str().unwrap_or("unknown")
+ ));
+ }
+ let mut res = res.error_for_status()?;
+ Ok(res.copy_to(output)?)
+ }
+
+ pub fn post(
+ &self,
+ nsid: &Nsid,
+ params: Option<HashMap<String, String>>,
+ body: Option<Value>,
+ ) -> Result<Option<Value>> {
+ let params: HashMap<String, String> = params.unwrap_or_default();
+ log::debug!(
+ "XRPC POST endpoint={} params={:?} body={:?}",
+ nsid,
+ params,
+ body
+ );
+ let mut req = self
+ .http_client
+ .post(format!("{}/xrpc/{}", self.host, nsid))
+ .query(&params);
+ req = if let Some(b) = body {
+ req.json(&b)
+ } else {
+ req
+ };
+ let res = req.send()?;
+ if res.status() == 400 {
+ let val: Value = res.json()?;
+ return Err(anyhow!(
+ "XRPC Bad Request (400): {}",
+ val["message"].as_str().unwrap_or("unknown")
+ ));
+ } else if res.status() == 500 {
+ let val: Value = res.json()?;
+ return Err(anyhow!(
+ "XRPC Internal Error (500): {}",
+ val["message"].as_str().unwrap_or("unknown")
+ ));
+ }
+ let res = res.error_for_status()?;
+ if res.content_length() == Some(0) {
+ Ok(None)
+ } else {
+ Ok(res.json()?)
+ }
+ }
+
+ pub fn post_cbor_from_reader<R: std::io::Read>(
+ &self,
+ nsid: &Nsid,
+ params: Option<HashMap<String, String>>,
+ input: &mut R,
+ ) -> Result<Option<Value>> {
+ let params: HashMap<String, String> = params.unwrap_or_default();
+ let mut buf: Vec<u8> = Vec::new();
+ input.read_to_end(&mut buf)?;
+ let res = self
+ .http_client
+ .post(format!("{}/xrpc/{}", self.host, nsid))
+ .query(&params)
+ .header(reqwest::header::CONTENT_TYPE, "application/cbor")
+ .body(buf)
+ .send()?;
+ if res.status() == 400 {
+ let val: Value = res.json()?;
+ return Err(anyhow!(
+ "XRPC Bad Request: {}",
+ val["message"].as_str().unwrap_or("unknown")
+ ));
+ }
+ let res = res.error_for_status()?;
+ Ok(res.json()?)
+ }
+
+ // reqwest::blocking::Body
+}
diff --git a/adenosine/tests/bigger.car b/adenosine/tests/bigger.car
new file mode 100644
index 0000000..7169013
--- /dev/null
+++ b/adenosine/tests/bigger.car
Binary files differ
diff --git a/adenosine/tests/example_repo.car b/adenosine/tests/example_repo.car
new file mode 100644
index 0000000..b2ae723
--- /dev/null
+++ b/adenosine/tests/example_repo.car
Binary files differ
diff --git a/adenosine/tests/test_mst_interop.rs b/adenosine/tests/test_mst_interop.rs
new file mode 100644
index 0000000..ee45019
--- /dev/null
+++ b/adenosine/tests/test_mst_interop.rs
@@ -0,0 +1,152 @@
+use adenosine::repo::RepoStore;
+use libipld::Cid;
+use std::collections::BTreeMap;
+use std::str::FromStr;
+
+#[test]
+fn test_known_maps() {
+ let mut repo = RepoStore::open_ephemeral().unwrap();
+ let cid1 =
+ Cid::from_str("bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454").unwrap();
+
+ let empty_map: BTreeMap<String, Cid> = Default::default();
+ assert_eq!(
+ repo.mst_from_map(&empty_map).unwrap().to_string(),
+ "bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm"
+ );
+
+ let mut trivial_map: BTreeMap<String, Cid> = Default::default();
+ trivial_map.insert("asdf".to_string(), cid1.clone());
+ assert_eq!(
+ repo.mst_from_map(&trivial_map).unwrap().to_string(),
+ "bafyreidaftbr35xhh4lzmv5jcoeufqjh75ohzmz6u56v7n2ippbtxdgqqe"
+ );
+
+ let mut singlelayer2_map: BTreeMap<String, Cid> = Default::default();
+ singlelayer2_map.insert("com.example.record/9ba1c7247ede".to_string(), cid1.clone());
+ assert_eq!(
+ repo.mst_from_map(&singlelayer2_map).unwrap().to_string(),
+ "bafyreid4g5smj6ukhrjasebt6myj7wmtm2eijouteoyueoqgoh6vm5jkae"
+ );
+
+ let mut simple_map: BTreeMap<String, Cid> = Default::default();
+ simple_map.insert("asdf".to_string(), cid1.clone());
+ simple_map.insert("88bfafc7".to_string(), cid1.clone());
+ simple_map.insert("2a92d355".to_string(), cid1.clone());
+ simple_map.insert("app.bsky.feed.post/454397e440ec".to_string(), cid1.clone());
+ simple_map.insert("app.bsky.feed.post/9adeb165882c".to_string(), cid1.clone());
+ assert_eq!(
+ repo.mst_from_map(&simple_map).unwrap().to_string(),
+ "bafyreiecb33zh7r2sc3k2wthm6exwzfktof63kmajeildktqc25xj6qzx4"
+ );
+}
+
+// TODO: behavior of these wide-char keys is undefined behavior in string MST
+#[ignore]
+#[test]
+fn test_tricky_map() {
+ let mut repo = RepoStore::open_ephemeral().unwrap();
+ let cid1 =
+ Cid::from_str("bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454").unwrap();
+
+ let mut tricky_map: BTreeMap<String, Cid> = Default::default();
+ tricky_map.insert("".to_string(), cid1.clone());
+ tricky_map.insert("jalapeño".to_string(), cid1.clone());
+ tricky_map.insert("coöperative".to_string(), cid1.clone());
+ tricky_map.insert("coüperative".to_string(), cid1.clone());
+ tricky_map.insert("abc\x00".to_string(), cid1.clone());
+ assert_eq!(
+ repo.mst_from_map(&tricky_map).unwrap().to_string(),
+ "bafyreiecb33zh7r2sc3k2wthm6exwzfktof63kmajeildktqc25xj6qzx4"
+ );
+}
+
+#[test]
+fn test_trims_top() {
+ // "trims top of tree on delete"
+
+ use adenosine::mst::print_mst_keys;
+ let mut repo = RepoStore::open_ephemeral().unwrap();
+ let cid1 =
+ Cid::from_str("bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454").unwrap();
+ let l1root = "bafyreihuyj2vzb2vjw3yhxg6dy25achg5fmre6gg5m6fjtxn64bqju4dee";
+ let l0root = "bafyreibmijjc63mekkjzl3v2pegngwke5u6cu66g75z6uw27v64bc6ahqi";
+
+ // NOTE: this test doesn't do much in this case of rust implementation
+ let mut trim_map: BTreeMap<String, Cid> = Default::default();
+ trim_map.insert("com.example.record/40c73105b48f".to_string(), cid1.clone()); // level 0
+ trim_map.insert("com.example.record/e99bf3ced34b".to_string(), cid1.clone()); // level 0
+ trim_map.insert("com.example.record/893e6c08b450".to_string(), cid1.clone()); // level 0
+ trim_map.insert("com.example.record/9cd8b6c0cc02".to_string(), cid1.clone()); // level 0
+ trim_map.insert("com.example.record/cbe72d33d12a".to_string(), cid1.clone()); // level 0
+ trim_map.insert("com.example.record/a15e33ba0f6c".to_string(), cid1.clone()); // level 1
+ let trim_before_cid = repo.mst_from_map(&trim_map).unwrap();
+ print_mst_keys(&mut repo.db, &trim_before_cid).unwrap();
+ assert_eq!(trim_before_cid.to_string(), l1root);
+
+ // NOTE: if we did mutations in-place, this is where we would mutate
+
+ trim_map.remove("com.example.record/a15e33ba0f6c");
+ let trim_after_cid = repo.mst_from_map(&trim_map).unwrap();
+ assert_eq!(trim_after_cid.to_string(), l0root);
+}
+
+#[test]
+fn test_insertion() {
+ // "handles insertion that splits two layers down"
+
+ let mut repo = RepoStore::open_ephemeral().unwrap();
+ let cid1 =
+ Cid::from_str("bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454").unwrap();
+ let l1root = "bafyreiagt55jzvkenoa4yik77dhomagq2uj26ix4cijj7kd2py2u3s43ve";
+ let l2root = "bafyreiddrz7qbvfattp5dzzh4ldohsaobatsg7f5l6awxnmuydewq66qoa";
+
+ // TODO: actual mutation instead of rebuild from scratch
+ let mut insertion_map: BTreeMap<String, Cid> = Default::default();
+ insertion_map.insert("com.example.record/403e2aeebfdb".to_string(), cid1.clone()); // A; level 0
+ insertion_map.insert("com.example.record/40c73105b48f".to_string(), cid1.clone()); // B; level 0
+ insertion_map.insert("com.example.record/645787eb4316".to_string(), cid1.clone()); // C; level 0
+ insertion_map.insert("com.example.record/7ca4e61d6fbc".to_string(), cid1.clone()); // D; level 1
+ insertion_map.insert("com.example.record/893e6c08b450".to_string(), cid1.clone()); // E; level 0
+ insertion_map.insert("com.example.record/9cd8b6c0cc02".to_string(), cid1.clone()); // G; level 0
+ insertion_map.insert("com.example.record/cbe72d33d12a".to_string(), cid1.clone()); // H; level 0
+ insertion_map.insert("com.example.record/dbea731be795".to_string(), cid1.clone()); // I; level 1
+ insertion_map.insert("com.example.record/e2ef555433f2".to_string(), cid1.clone()); // J; level 0
+ insertion_map.insert("com.example.record/e99bf3ced34b".to_string(), cid1.clone()); // K; level 0
+ insertion_map.insert("com.example.record/f728ba61e4b6".to_string(), cid1.clone()); // L; level 0
+ let insertion_before_cid = repo.mst_from_map(&insertion_map).unwrap();
+ assert_eq!(insertion_before_cid.to_string(), l1root);
+
+ insertion_map.insert("com.example.record/9ba1c7247ede".to_string(), cid1.clone());
+ let insertion_after_cid = repo.mst_from_map(&insertion_map).unwrap();
+ assert_eq!(insertion_after_cid.to_string(), l2root);
+}
+
+#[test]
+fn test_higher_layers() {
+ // "handles new layers that are two higher than existing"
+
+ use adenosine::mst::print_mst_keys;
+ let mut repo = RepoStore::open_ephemeral().unwrap();
+ let cid1 =
+ Cid::from_str("bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454").unwrap();
+ let l0root = "bafyreicivoa3p3ttcebdn2zfkdzenkd2uk3gxxlaz43qvueeip6yysvq2m";
+ let l2root = "bafyreidwoqm6xlewxzhrx6ytbyhsazctlv72txtmnd4au6t53z2vpzn7wa";
+ let l2root2 = "bafyreiapru27ce4wdlylk5revtr3hewmxhmt3ek5f2ypioiivmdbv5igrm";
+
+ // TODO: actual mutation instead of rebuild from scratch
+ let mut higher_map: BTreeMap<String, Cid> = Default::default();
+ higher_map.insert("com.example.record/403e2aeebfdb".to_string(), cid1.clone()); // A; level 0
+ higher_map.insert("com.example.record/cbe72d33d12a".to_string(), cid1.clone()); // C; level 0
+ let higher_before_cid = repo.mst_from_map(&higher_map).unwrap();
+ assert_eq!(higher_before_cid.to_string(), l0root);
+
+ higher_map.insert("com.example.record/9ba1c7247ede".to_string(), cid1.clone()); // B; level 2
+ let higher_after_cid = repo.mst_from_map(&higher_map).unwrap();
+ print_mst_keys(&mut repo.db, &higher_after_cid).unwrap();
+ assert_eq!(higher_after_cid.to_string(), l2root);
+
+ higher_map.insert("com.example.record/fae7a851fbeb".to_string(), cid1.clone()); // D; level 1
+ let higher_after_cid = repo.mst_from_map(&higher_map).unwrap();
+ assert_eq!(higher_after_cid.to_string(), l2root2);
+}
diff --git a/adenosine/tests/test_repro_mst.rs b/adenosine/tests/test_repro_mst.rs
new file mode 100644
index 0000000..692c41b
--- /dev/null
+++ b/adenosine/tests/test_repro_mst.rs
@@ -0,0 +1,26 @@
+use adenosine::repo::RepoStore;
+use std::path::PathBuf;
+use std::str::FromStr;
+
+#[test]
+fn test_repro_mst() {
+ let mut repo = RepoStore::open_ephemeral().unwrap();
+ let cid = repo
+ .import_car_path(
+ &PathBuf::from_str("./tests/example_repo.car").unwrap(),
+ None,
+ )
+ .unwrap();
+ repo.verify_repo_mst(&cid).unwrap();
+ let cid = repo
+ .import_car_path(&PathBuf::from_str("./tests/bigger.car").unwrap(), None)
+ .unwrap();
+ repo.verify_repo_mst(&cid).unwrap();
+
+ // test round-tripping from export
+ let car_bytes = repo.export_car(&cid, None).unwrap();
+ let mut other_repo = RepoStore::open_ephemeral().unwrap();
+ let other_cid = other_repo.import_car_bytes(&car_bytes, None).unwrap();
+ other_repo.verify_repo_mst(&cid).unwrap();
+ assert_eq!(cid, other_cid);
+}