aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--adenosine-cli/src/bin/adenosine.rs4
-rw-r--r--adenosine-pds/Cargo.toml32
-rw-r--r--adenosine-pds/src/bin/adenosine-pds.rs6
-rw-r--r--adenosine-pds/src/db.rs15
-rw-r--r--adenosine-pds/src/db_bsky.rs (renamed from adenosine-pds/src/bsky.rs)71
-rw-r--r--adenosine-pds/src/lib.rs134
-rw-r--r--adenosine-pds/src/web.rs11
-rw-r--r--adenosine/Cargo.toml37
-rw-r--r--adenosine/src/app_bsky/mod.rs (renamed from adenosine-pds/src/models.rs)83
-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.car (renamed from adenosine-pds/tests/bigger.car)bin60050 -> 60050 bytes
-rw-r--r--adenosine/tests/example_repo.car (renamed from adenosine-pds/tests/example_repo.car)bin7390 -> 7390 bytes
-rw-r--r--adenosine/tests/test_mst_interop.rs (renamed from adenosine-pds/tests/test_mst_interop.rs)6
-rw-r--r--adenosine/tests/test_repro_mst.rs (renamed from adenosine-pds/tests/test_repro_mst.rs)2
20 files changed, 498 insertions, 486 deletions
diff --git a/adenosine-cli/src/bin/adenosine.rs b/adenosine-cli/src/bin/adenosine.rs
index 4164181..e35d634 100644
--- a/adenosine-cli/src/bin/adenosine.rs
+++ b/adenosine-cli/src/bin/adenosine.rs
@@ -1,5 +1,7 @@
+use adenosine::auth::parse_did_from_jwt;
+use adenosine::created_at_now;
use adenosine::identifiers::*;
-use adenosine::*;
+use adenosine::xrpc::{XrpcClient, XrpcMethod};
use adenosine_cli::*;
use anyhow::anyhow;
use serde_json::{json, Value};
diff --git a/adenosine-pds/Cargo.toml b/adenosine-pds/Cargo.toml
index 41cfaae..f9dba1a 100644
--- a/adenosine-pds/Cargo.toml
+++ b/adenosine-pds/Cargo.toml
@@ -13,32 +13,24 @@ readme = "README.md"
repository = "https://gitlab.com/bnewbold/adenosine"
[dependencies]
+adenosine = { version = "0.2.0", path = "../adenosine" }
anyhow = "1"
-structopt = "0.3"
-serde = "1"
-serde_json = "1"
+askama = { version = "0.11", features = ["serde-json"] }
+bcrypt = "0.13"
+data-encoding = "2"
+dotenvy = "0.15"
+#ipfs-sqlite-block-store = "0.13"
+lazy_static = "1"
+libipld = { version = "0.14", features = ["dag-cbor", "derive"] }
log = "0.4"
pretty_env_logger = "0.4"
-libipld = "0.14"
-ipfs-sqlite-block-store = "0.13"
+rouille = "3"
rusqlite = { version = "0.26", features = ["bundled"] }
rusqlite_migration = "1"
-# NOTE: lexicon validation not implemented yet
-#jsonschema = "*"
-#schemafy = "*"
-rouille = "3"
-# NOTE: vendored for now
-#iroh-car = "*"
-adenosine = { version = "0.2.0", path = "../adenosine" }
+serde = "1"
+serde_json = "1"
+structopt = "0.3"
tokio = { version = "1", features = ["full"] }
-futures = "0.3"
-lazy_static = "1"
-bcrypt = "0.13"
-data-encoding = "2"
-# TODO: replace this with data-encoding or similar; this is only needed for ucan_p256 stuff
-async-trait = "0.1"
-dotenvy = "0.15"
-askama = { version = "0.11", features = ["serde-json"] }
[package.metadata.deb]
maintainer = "Bryan Newbold <bnewbold@robocracy.org>"
diff --git a/adenosine-pds/src/bin/adenosine-pds.rs b/adenosine-pds/src/bin/adenosine-pds.rs
index c16e7e0..c004098 100644
--- a/adenosine-pds/src/bin/adenosine-pds.rs
+++ b/adenosine-pds/src/bin/adenosine-pds.rs
@@ -1,5 +1,7 @@
+use adenosine::com_atproto;
+use adenosine::crypto::KeyPair;
use adenosine::mst;
-use adenosine_pds::models::AccountRequest;
+use adenosine::repo::RepoStore;
use adenosine_pds::*;
use anyhow::Result;
use serde_json::json;
@@ -208,7 +210,7 @@ fn main() -> Result<()> {
public_url,
did_plc,
} => {
- let req = AccountRequest {
+ let req = com_atproto::AccountRequest {
email,
handle: handle.clone(),
password,
diff --git a/adenosine-pds/src/db.rs b/adenosine-pds/src/db.rs
index b694bb5..4cbece6 100644
--- a/adenosine-pds/src/db.rs
+++ b/adenosine-pds/src/db.rs
@@ -1,6 +1,7 @@
-use crate::models::{FollowRecord, Post, RefRecord};
/// ATP database (as distinct from blockstore)
-use crate::{created_at_now, ipld_into_json_value, AtpSession, Did, KeyPair, Tid};
+use crate::{created_at_now, ipld_into_json_value, Did, KeyPair, Tid};
+use adenosine::app_bsky;
+use adenosine::com_atproto;
use anyhow::{anyhow, Result};
use lazy_static::lazy_static;
use libipld::cbor::DagCborCodec;
@@ -105,7 +106,7 @@ impl AtpDatabase {
handle: &str,
password: &str,
keypair: &KeyPair,
- ) -> Result<AtpSession> {
+ ) -> Result<com_atproto::Session> {
let mut stmt = self
.conn
.prepare_cached("SELECT did, password_bcrypt FROM account WHERE handle = ?1")?;
@@ -120,7 +121,7 @@ impl AtpDatabase {
.conn
.prepare_cached("INSERT INTO session (did, jwt) VALUES (?1, ?2)")?;
stmt.execute(params!(did.to_string(), jwt))?;
- Ok(AtpSession {
+ Ok(com_atproto::Session {
did: did.to_string(),
name: handle.to_string(),
accessJwt: jwt.to_string(),
@@ -192,7 +193,7 @@ impl AtpDatabase {
// need to re-compute the CID from DagCbor re-encoding, I guess. bleh.
let block = Block::<DefaultParams>::encode(DagCborCodec, Code::Sha2_256, &val)?;
let cid = *block.cid();
- let post: Post = serde_json::from_value(ipld_into_json_value(val))?;
+ let post: app_bsky::Post = serde_json::from_value(ipld_into_json_value(val))?;
let (reply_to_parent_uri, reply_to_root_uri) = match post.reply {
Some(ref reply) => (Some(reply.parent.uri.clone()), Some(reply.root.uri.clone())),
None => (None, None),
@@ -226,7 +227,7 @@ impl AtpDatabase {
val: Option<Ipld>,
) -> Result<()> {
if let Some(val) = val {
- let ref_obj: RefRecord = serde_json::from_value(ipld_into_json_value(val))?;
+ let ref_obj: app_bsky::RefRecord = serde_json::from_value(ipld_into_json_value(val))?;
let mut stmt = self
.conn
.prepare_cached("INSERT INTO bsky_ref (ref_type, did, tid, subject_uri, subject_cid, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)")?;
@@ -253,7 +254,7 @@ impl AtpDatabase {
pub fn bsky_upsert_follow(&mut self, did: &Did, tid: &Tid, val: Option<Ipld>) -> Result<()> {
if let Some(val) = val {
- let follow: FollowRecord = serde_json::from_value(ipld_into_json_value(val))?;
+ let follow: app_bsky::FollowRecord = serde_json::from_value(ipld_into_json_value(val))?;
let mut stmt = self
.conn
.prepare_cached("INSERT INTO bsky_follow (did, tid, subject_did, created_at) VALUES (?1, ?2, ?3, ?4)")?;
diff --git a/adenosine-pds/src/bsky.rs b/adenosine-pds/src/db_bsky.rs
index caa16f6..ee8e0f3 100644
--- a/adenosine-pds/src/bsky.rs
+++ b/adenosine-pds/src/db_bsky.rs
@@ -1,11 +1,9 @@
-use crate::models::*;
/// Helper functions for doing database and repo operations relating to bluesky endpoints and
/// records
-use crate::{
- ipld_into_json_value, json_value_into_ipld, AtpDatabase, AtpService, Did, Result, Tid,
- XrpcError,
-};
-use adenosine::identifiers::{AtUri, DidOrHost, Nsid};
+use crate::{AtpDatabase, AtpService, Result, XrpcError};
+use adenosine::app_bsky;
+use adenosine::identifiers::{AtUri, Did, DidOrHost, Nsid, Tid};
+use adenosine::ipld::{ipld_into_json_value, json_value_into_ipld};
use adenosine::repo::Mutation;
use anyhow::anyhow;
use libipld::Cid;
@@ -61,7 +59,7 @@ pub fn bsky_mutate_db(db: &mut AtpDatabase, did: &Did, mutations: Vec<Mutation>)
}
// TODO: should probably return Result<Option<Profile>>?
-pub fn bsky_get_profile(srv: &mut AtpService, did: &Did) -> Result<Profile> {
+pub fn bsky_get_profile(srv: &mut AtpService, did: &Did) -> Result<app_bsky::Profile> {
// first get the profile record
let mut profile_cid: Option<Cid> = None;
let commit_cid = match srv.repo.lookup_commit(did)? {
@@ -78,7 +76,7 @@ pub fn bsky_get_profile(srv: &mut AtpService, did: &Did) -> Result<Profile> {
}
let (display_name, description): (Option<String>, Option<String>) =
if let Some(cid) = profile_cid {
- let record: ProfileRecord =
+ let record: app_bsky::ProfileRecord =
serde_json::from_value(ipld_into_json_value(srv.repo.get_ipld(&cid)?))?;
(Some(record.displayName), record.description)
} else {
@@ -104,11 +102,11 @@ pub fn bsky_get_profile(srv: &mut AtpService, did: &Did) -> Result<Profile> {
.conn
.prepare_cached("SELECT COUNT(*) FROM bsky_follow WHERE subject_did = $1")?;
let followers_count: u64 = stmt.query_row(params!(did.to_string()), |row| row.get(0))?;
- let decl = DeclRef {
+ let decl = app_bsky::DeclRef {
actorType: "app.bsky.system.actorUser".to_string(),
cid: "bafyreid27zk7lbis4zw5fz4podbvbs4fc5ivwji3dmrwa6zggnj4bnd57u".to_string(),
};
- Ok(Profile {
+ Ok(app_bsky::Profile {
did: did.to_string(),
handle,
creator: did.to_string(),
@@ -123,7 +121,11 @@ pub fn bsky_get_profile(srv: &mut AtpService, did: &Did) -> Result<Profile> {
})
}
-pub fn bsky_update_profile(srv: &mut AtpService, did: &Did, profile: ProfileRecord) -> Result<()> {
+pub fn bsky_update_profile(
+ srv: &mut AtpService,
+ did: &Did,
+ profile: app_bsky::ProfileRecord,
+) -> Result<()> {
// get the profile record
let mut profile_tid: Option<Tid> = None;
let commit_cid = match srv.repo.lookup_commit(did)? {
@@ -175,7 +177,7 @@ fn feed_row(row: &rusqlite::Row) -> Result<FeedRow> {
})
}
-fn feed_row_to_item(srv: &mut AtpService, row: FeedRow) -> Result<FeedItem> {
+fn feed_row_to_item(srv: &mut AtpService, row: FeedRow) -> Result<app_bsky::FeedItem> {
let record_ipld = srv.repo.get_ipld(&row.item_post_cid)?;
let uri = format!(
"at://{}/{}/{}",
@@ -198,10 +200,10 @@ fn feed_row_to_item(srv: &mut AtpService, row: FeedRow) -> Result<FeedItem> {
.prepare_cached("SELECT COUNT(*) FROM bsky_post WHERE reply_to_parent_uri = $1")?;
let reply_count: u64 = stmt.query_row(params!(uri), |row| row.get(0))?;
- let feed_item = FeedItem {
+ let feed_item = app_bsky::FeedItem {
uri,
cid: row.item_post_cid.to_string(),
- author: User {
+ author: app_bsky::User {
did: row.item_did.to_string(),
handle: row.item_handle,
displayName: None, // TODO: fetch from profile (or cache)
@@ -219,8 +221,8 @@ fn feed_row_to_item(srv: &mut AtpService, row: FeedRow) -> Result<FeedItem> {
Ok(feed_item)
}
-pub fn bsky_get_timeline(srv: &mut AtpService, did: &Did) -> Result<GenericFeed> {
- let mut feed: Vec<FeedItem> = vec![];
+pub fn bsky_get_timeline(srv: &mut AtpService, did: &Did) -> Result<app_bsky::GenericFeed> {
+ let mut feed: Vec<app_bsky::FeedItem> = vec![];
// TODO: also handle reposts
let rows = {
let mut stmt = srv.atp_db
@@ -237,11 +239,11 @@ pub fn bsky_get_timeline(srv: &mut AtpService, did: &Did) -> Result<GenericFeed>
for row in rows {
feed.push(feed_row_to_item(srv, row)?);
}
- Ok(GenericFeed { feed })
+ Ok(app_bsky::GenericFeed { feed })
}
-pub fn bsky_get_author_feed(srv: &mut AtpService, did: &Did) -> Result<GenericFeed> {
- let mut feed: Vec<FeedItem> = vec![];
+pub fn bsky_get_author_feed(srv: &mut AtpService, did: &Did) -> Result<app_bsky::GenericFeed> {
+ let mut feed: Vec<app_bsky::FeedItem> = vec![];
// TODO: also handle reposts
let rows = {
let mut stmt = srv.atp_db
@@ -258,7 +260,7 @@ pub fn bsky_get_author_feed(srv: &mut AtpService, did: &Did) -> Result<GenericFe
for row in rows {
feed.push(feed_row_to_item(srv, row)?);
}
- Ok(GenericFeed { feed })
+ Ok(app_bsky::GenericFeed { feed })
}
// TODO: this is a partial implementation
@@ -267,7 +269,7 @@ pub fn bsky_get_thread(
srv: &mut AtpService,
uri: &AtUri,
_depth: Option<u64>,
-) -> Result<PostThread> {
+) -> Result<app_bsky::PostThread> {
// parse the URI
let did = match uri.repository {
DidOrHost::Did(ref did_type, ref did_body) => {
@@ -283,7 +285,7 @@ pub fn bsky_get_thread(
_ => Err(anyhow!("expected a record in uri: {}", uri))?,
};
- // post itself, as a FeedItem
+ // post itself, as a app_bsky::FeedItem
let post_items = {
let mut stmt = srv.atp_db
.conn
@@ -320,7 +322,7 @@ pub fn bsky_get_thread(
};
for row in rows {
let item = feed_row_to_item(srv, row)?;
- children.push(ThreadItem {
+ children.push(app_bsky::ThreadItem {
uri: item.uri,
cid: item.cid,
author: item.author,
@@ -339,7 +341,7 @@ pub fn bsky_get_thread(
});
}
- let post = ThreadItem {
+ let post = app_bsky::ThreadItem {
uri: post_item.uri,
cid: post_item.cid,
author: post_item.author,
@@ -354,19 +356,20 @@ pub fn bsky_get_thread(
indexedAt: post_item.indexedAt,
myState: None,
};
- Ok(PostThread { thread: post })
+ Ok(app_bsky::PostThread { thread: post })
}
#[test]
fn test_bsky_profile() {
use crate::{create_account, created_at_now};
+ use adenosine::com_atproto;
use libipld::ipld;
let post_nsid = Nsid::from_str("app.bsky.feed.post").unwrap();
let follow_nsid = Nsid::from_str("app.bsky.graph.follow").unwrap();
let mut srv = AtpService::new_ephemeral().unwrap();
- let req = AccountRequest {
+ let req = com_atproto::AccountRequest {
email: "test@bogus.com".to_string(),
handle: "handle.test".to_string(),
password: "bogus".to_string(),
@@ -384,7 +387,7 @@ fn test_bsky_profile() {
assert_eq!(profile.followsCount, 0);
assert_eq!(profile.postsCount, 0);
- let record = ProfileRecord {
+ let record = app_bsky::ProfileRecord {
displayName: "Test Name".to_string(),
description: Some("short description".to_string()),
};
@@ -393,7 +396,7 @@ fn test_bsky_profile() {
assert_eq!(profile.displayName, Some(record.displayName));
assert_eq!(profile.description, record.description);
- let record = ProfileRecord {
+ let record = app_bsky::ProfileRecord {
displayName: "New Test Name".to_string(),
description: Some("longer description".to_string()),
};
@@ -444,6 +447,7 @@ fn test_bsky_profile() {
fn test_bsky_feeds() {
// TODO: test that displayName comes through in feeds and timelines (it does not currently)
use crate::{create_account, created_at_now};
+ use adenosine::com_atproto;
use libipld::ipld;
let post_nsid = Nsid::from_str("app.bsky.feed.post").unwrap();
@@ -453,7 +457,7 @@ fn test_bsky_feeds() {
let mut srv = AtpService::new_ephemeral().unwrap();
let alice_did = {
- let req = AccountRequest {
+ let req = com_atproto::AccountRequest {
email: "alice@bogus.com".to_string(),
handle: "alice.test".to_string(),
password: "bogus".to_string(),
@@ -464,7 +468,7 @@ fn test_bsky_feeds() {
Did::from_str(&session.did).unwrap()
};
let bob_did = {
- let req = AccountRequest {
+ let req = com_atproto::AccountRequest {
email: "bob@bogus.com".to_string(),
handle: "bob.test".to_string(),
password: "bogus".to_string(),
@@ -475,7 +479,7 @@ fn test_bsky_feeds() {
Did::from_str(&session.did).unwrap()
};
let carol_did = {
- let req = AccountRequest {
+ let req = com_atproto::AccountRequest {
email: "carol@bogus.com".to_string(),
handle: "carol.test".to_string(),
password: "bogus".to_string(),
@@ -672,13 +676,14 @@ fn test_bsky_feeds() {
#[test]
fn test_bsky_thread() {
use crate::create_account;
+ use adenosine::com_atproto;
use libipld::ipld;
let post_nsid = Nsid::from_str("app.bsky.feed.post").unwrap();
let mut srv = AtpService::new_ephemeral().unwrap();
let alice_did = {
- let req = AccountRequest {
+ let req = com_atproto::AccountRequest {
email: "alice@bogus.com".to_string(),
handle: "alice.test".to_string(),
password: "bogus".to_string(),
@@ -689,7 +694,7 @@ fn test_bsky_thread() {
Did::from_str(&session.did).unwrap()
};
let bob_did = {
- let req = AccountRequest {
+ let req = com_atproto::AccountRequest {
email: "bob@bogus.com".to_string(),
handle: "bob.test".to_string(),
password: "bogus".to_string(),
diff --git a/adenosine-pds/src/lib.rs b/adenosine-pds/src/lib.rs
index 07905f9..cca9b0d 100644
--- a/adenosine-pds/src/lib.rs
+++ b/adenosine-pds/src/lib.rs
@@ -2,33 +2,51 @@ use adenosine::created_at_now;
use adenosine::identifiers::{AtUri, Did, Nsid, Ticker, Tid};
use anyhow::{anyhow, Result};
use askama::Template;
-use libipld::Cid;
-use libipld::Ipld;
use log::{debug, error, info, warn};
use rouille::{router, Request, Response};
use serde_json::{json, Value};
-use std::collections::BTreeMap;
use std::fmt;
use std::io::Read;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Mutex;
-mod bsky;
mod db;
-pub mod models;
+mod db_bsky;
mod web;
-pub use adenosine::crypto::{KeyPair, PubKey};
-pub use adenosine::did;
-pub use adenosine::did::DidDocMeta;
-pub use adenosine::repo::{Mutation, RepoCommit, RepoStore};
-pub use adenosine::ucan_p256::P256KeyMaterial;
-use bsky::*;
-pub use db::AtpDatabase;
-pub use models::*;
+use adenosine::app_bsky;
+use adenosine::com_atproto;
+use adenosine::crypto::KeyPair;
+use adenosine::ipld::{ipld_into_json_value, json_value_into_ipld};
+use adenosine::plc;
+use adenosine::plc::DidDocMeta;
+use adenosine::repo::{Mutation, RepoStore};
+use db::AtpDatabase;
+use db_bsky::*;
use web::*;
+#[derive(Debug)]
+pub enum XrpcError {
+ BadRequest(String),
+ NotFound(String),
+ Forbidden(String),
+ MutexPoisoned,
+}
+
+impl std::error::Error for XrpcError {}
+
+impl fmt::Display for XrpcError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::BadRequest(msg) | Self::NotFound(msg) | Self::Forbidden(msg) => {
+ write!(f, "{}", msg)
+ }
+ Self::MutexPoisoned => write!(f, "service mutex poisoned"),
+ }
+ }
+}
+
pub struct AtpService {
pub repo: RepoStore,
pub atp_db: AtpDatabase,
@@ -58,27 +76,6 @@ impl Default for AtpServiceConfig {
}
}
-#[derive(Debug)]
-enum XrpcError {
- BadRequest(String),
- NotFound(String),
- Forbidden(String),
- MutexPoisoned,
-}
-
-impl std::error::Error for XrpcError {}
-
-impl fmt::Display for XrpcError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::BadRequest(msg) | Self::NotFound(msg) | Self::Forbidden(msg) => {
- write!(f, "{}", msg)
- }
- Self::MutexPoisoned => write!(f, "service mutex poisoned"),
- }
- }
-}
-
/// Helper to take an XRPC result (always a JSON object), and transform it to a rouille response
fn xrpc_wrap<S: serde::Serialize>(resp: Result<S>) -> Response {
match resp {
@@ -256,49 +253,6 @@ impl AtpService {
}
}
-/// 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.
-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).
-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)
- }
- }
-}
-
fn xrpc_required_param(request: &Request, key: &str) -> Result<String> {
Ok(request.get_param(key).ok_or(XrpcError::BadRequest(format!(
"require '{}' query parameter",
@@ -424,7 +378,7 @@ fn xrpc_get_handler(
let mut srv = srv.lock().or(Err(XrpcError::MutexPoisoned))?;
let did_doc = srv.atp_db.get_did_doc(&did)?;
let collections: Vec<String> = srv.repo.collections(&did)?;
- let desc = RepoDescribe {
+ let desc = com_atproto::repo::Describe {
name: did.to_string(), // TODO: handle?
did: did.to_string(),
didDoc: did_doc,
@@ -506,9 +460,9 @@ fn xrpc_get_repo_handler(srv: &Mutex<AtpService>, request: &Request) -> Result<V
pub fn create_account(
srv: &mut AtpService,
- req: &AccountRequest,
+ req: &com_atproto::AccountRequest,
create_did_plc: bool,
-) -> Result<AtpSession> {
+) -> Result<com_atproto::Session> {
// check if account already exists (fast path, also confirmed by database schema)
if srv.atp_db.account_exists(&req.handle, &req.email)? {
Err(XrpcError::BadRequest(
@@ -520,7 +474,7 @@ pub fn create_account(
let (did, did_doc) = if create_did_plc {
// generate DID
- let create_op = did::CreateOp::new(
+ let create_op = plc::CreateOp::new(
req.handle.clone(),
srv.config.public_url.clone(),
&srv.pds_keypair,
@@ -576,7 +530,7 @@ fn xrpc_post_handler(
match method {
"com.atproto.account.create" => {
// validate account request
- let req: AccountRequest = rouille::input::json_input(request)
+ let req: com_atproto::AccountRequest = rouille::input::json_input(request)
.map_err(|e| XrpcError::BadRequest(format!("failed to parse JSON body: {}", e)))?;
// TODO: validate handle, email, recoverykey
let mut srv = srv.lock().unwrap();
@@ -602,7 +556,7 @@ fn xrpc_post_handler(
Ok(json!(sess))
}
"com.atproto.session.create" => {
- let req: SessionRequest = rouille::input::json_input(request)
+ let req: com_atproto::SessionRequest = rouille::input::json_input(request)
.map_err(|e| XrpcError::BadRequest(format!("failed to parse JSON body: {}", e)))?;
let mut srv = srv.lock().unwrap();
let keypair = srv.pds_keypair.clone();
@@ -628,7 +582,7 @@ fn xrpc_post_handler(
.resolve_did(&did)?
.expect("DID matches to a handle");
- Ok(json!(AtpSession {
+ Ok(json!(com_atproto::Session {
did: did.to_string(),
name: handle,
accessJwt: jwt.to_string(),
@@ -653,7 +607,7 @@ fn xrpc_post_handler(
Ok(json!({}))
}
"com.atproto.repo.batchWrite" => {
- let batch: RepoBatchWriteBody = rouille::input::json_input(request)?;
+ let batch: com_atproto::repo::BatchWriteBody = rouille::input::json_input(request)?;
// TODO: validate edits against schemas
let did = Did::from_str(&batch.did)?;
let mut srv = srv.lock().unwrap();
@@ -690,7 +644,7 @@ fn xrpc_post_handler(
}
"com.atproto.repo.createRecord" => {
// TODO: validate edits against schemas
- let create: RepoCreateRecord = rouille::input::json_input(request)?;
+ let create: com_atproto::repo::CreateRecord = rouille::input::json_input(request)?;
let did = Did::from_str(&create.did)?;
let collection = Nsid::from_str(&create.collection)?;
let mut srv = srv.lock().unwrap();
@@ -707,7 +661,7 @@ fn xrpc_post_handler(
}
"com.atproto.repo.putRecord" => {
// TODO: validate edits against schemas
- let put: RepoPutRecord = rouille::input::json_input(request)?;
+ let put: com_atproto::repo::PutRecord = rouille::input::json_input(request)?;
let did = Did::from_str(&put.did)?;
let collection = Nsid::from_str(&put.collection)?;
let tid = Tid::from_str(&put.rkey)?;
@@ -725,7 +679,7 @@ fn xrpc_post_handler(
Ok(json!({}))
}
"com.atproto.repo.deleteRecord" => {
- let delete: RepoDeleteRecord = rouille::input::json_input(request)?;
+ let delete: com_atproto::repo::DeleteRecord = rouille::input::json_input(request)?;
let did = Did::from_str(&delete.did)?;
let collection = Nsid::from_str(&delete.collection)?;
let tid = Tid::from_str(&delete.rkey)?;
@@ -754,7 +708,7 @@ fn xrpc_post_handler(
}
// =========== app.bsky methods
"app.bsky.actor.updateProfile" => {
- let profile: ProfileRecord = rouille::input::json_input(request)?;
+ let profile: app_bsky::ProfileRecord = rouille::input::json_input(request)?;
let mut srv = srv.lock().unwrap();
let auth_did = &xrpc_check_auth_header(&mut srv, request, None)?;
bsky_update_profile(&mut srv, auth_did, profile)?;
@@ -865,7 +819,7 @@ fn repo_view_handler(srv: &Mutex<AtpService>, did: &str, request: &Request) -> R
let commit_cid = &srv.repo.lookup_commit(&did)?.unwrap();
let commit = srv.repo.get_commit(commit_cid)?;
let collections: Vec<String> = srv.repo.collections(&did)?;
- let desc = RepoDescribe {
+ let desc = com_atproto::repo::Describe {
name: did.to_string(), // TODO
did: did.to_string(),
didDoc: did_doc,
diff --git a/adenosine-pds/src/web.rs b/adenosine-pds/src/web.rs
index 0d80b4e..8f15a49 100644
--- a/adenosine-pds/src/web.rs
+++ b/adenosine-pds/src/web.rs
@@ -1,4 +1,5 @@
-use crate::models::*;
+use adenosine::app_bsky;
+use adenosine::com_atproto;
use adenosine::identifiers::{Did, Nsid, Tid};
use adenosine::repo::RepoCommit;
use askama::Template;
@@ -28,8 +29,8 @@ pub struct AboutView {
pub struct AccountView {
pub domain: String,
pub did: Did,
- pub profile: Profile,
- pub feed: Vec<FeedItem>,
+ pub profile: app_bsky::Profile,
+ pub feed: Vec<app_bsky::FeedItem>,
}
#[derive(Template)]
@@ -39,7 +40,7 @@ pub struct ThreadView {
pub did: Did,
pub collection: Nsid,
pub tid: Tid,
- pub post: ThreadItem,
+ pub post: app_bsky::ThreadItem,
}
#[derive(Template)]
@@ -48,7 +49,7 @@ pub struct RepoView {
pub domain: String,
pub did: Did,
pub commit: RepoCommit,
- pub describe: RepoDescribe,
+ pub describe: com_atproto::repo::Describe,
}
#[derive(Template)]
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-pds/src/models.rs b/adenosine/src/app_bsky/mod.rs
index f827a7a..18a0449 100644
--- a/adenosine-pds/src/models.rs
+++ b/adenosine/src/app_bsky/mod.rs
@@ -1,87 +1,6 @@
+/// app.bsky types (manually entered)
use serde_json::Value;
-// =========== com.atproto types (manually entered)
-
-#[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 AtpSession {
- pub did: String,
- pub name: String,
- pub accessJwt: String,
- pub refreshJwt: String,
-}
-
-#[allow(non_snake_case)]
-#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
-pub struct RepoDescribe {
- 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 RepoCreateRecord {
- 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 RepoPutRecord {
- 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 RepoDeleteRecord {
- pub did: String,
- pub collection: String,
- pub rkey: String,
-}
-
-#[allow(non_snake_case)]
-#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
-pub struct RepoBatchWriteBody {
- pub did: String,
- pub writes: Vec<RepoBatchWrite>,
-}
-
-#[allow(non_snake_case)]
-#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
-pub struct RepoBatchWrite {
- #[serde(rename = "type")]
- pub op_type: String,
- pub collection: String,
- pub rkey: Option<String>,
- pub value: serde_json::Value,
-}
-
-// =========== app.bsky types (manually entered)
-
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
pub struct Subject {
pub uri: String,
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-pds/tests/bigger.car b/adenosine/tests/bigger.car
index 7169013..7169013 100644
--- a/adenosine-pds/tests/bigger.car
+++ b/adenosine/tests/bigger.car
Binary files differ
diff --git a/adenosine-pds/tests/example_repo.car b/adenosine/tests/example_repo.car
index b2ae723..b2ae723 100644
--- a/adenosine-pds/tests/example_repo.car
+++ b/adenosine/tests/example_repo.car
Binary files differ
diff --git a/adenosine-pds/tests/test_mst_interop.rs b/adenosine/tests/test_mst_interop.rs
index 8a5becc..ee45019 100644
--- a/adenosine-pds/tests/test_mst_interop.rs
+++ b/adenosine/tests/test_mst_interop.rs
@@ -1,4 +1,4 @@
-use adenosine_pds::RepoStore;
+use adenosine::repo::RepoStore;
use libipld::Cid;
use std::collections::BTreeMap;
use std::str::FromStr;
@@ -65,7 +65,7 @@ fn test_tricky_map() {
fn test_trims_top() {
// "trims top of tree on delete"
- use adenosine_pds::mst::print_mst_keys;
+ use adenosine::mst::print_mst_keys;
let mut repo = RepoStore::open_ephemeral().unwrap();
let cid1 =
Cid::from_str("bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454").unwrap();
@@ -126,7 +126,7 @@ fn test_insertion() {
fn test_higher_layers() {
// "handles new layers that are two higher than existing"
- use adenosine_pds::mst::print_mst_keys;
+ use adenosine::mst::print_mst_keys;
let mut repo = RepoStore::open_ephemeral().unwrap();
let cid1 =
Cid::from_str("bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454").unwrap();
diff --git a/adenosine-pds/tests/test_repro_mst.rs b/adenosine/tests/test_repro_mst.rs
index df88559..692c41b 100644
--- a/adenosine-pds/tests/test_repro_mst.rs
+++ b/adenosine/tests/test_repro_mst.rs
@@ -1,4 +1,4 @@
-use adenosine_pds::RepoStore;
+use adenosine::repo::RepoStore;
use std::path::PathBuf;
use std::str::FromStr;