Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Get radicle-desktop running on the desktop
Draft did:key:z6MkkfM3...sVz5 opened 1 year ago
45 files changed +4615 -812 e0323e34 d90dc073
modified .gitignore
@@ -3,6 +3,7 @@
node_modules/

# Tauri
+
crates/test-http-api/target
crates/radicle-tauri/target
crates/radicle-tauri/gen/schemas

modified Cargo.lock
@@ -292,7 +292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
dependencies = [
 "async-trait",
-
 "axum-core",
+
 "axum-core 0.3.4",
 "bitflags 1.3.2",
 "bytes",
 "futures-util",
@@ -308,7 +308,40 @@ dependencies = [
 "rustversion",
 "serde",
 "sync_wrapper 0.1.2",
-
 "tower",
+
 "tower 0.4.13",
+
 "tower-layer",
+
 "tower-service",
+
]
+

+
[[package]]
+
name = "axum"
+
version = "0.7.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae"
+
dependencies = [
+
 "async-trait",
+
 "axum-core 0.4.5",
+
 "bytes",
+
 "futures-util",
+
 "http 1.1.0",
+
 "http-body 1.0.1",
+
 "http-body-util",
+
 "hyper 1.5.0",
+
 "hyper-util",
+
 "itoa 1.0.11",
+
 "matchit",
+
 "memchr",
+
 "mime",
+
 "percent-encoding",
+
 "pin-project-lite",
+
 "rustversion",
+
 "serde",
+
 "serde_json",
+
 "serde_path_to_error",
+
 "serde_urlencoded",
+
 "sync_wrapper 1.0.1",
+
 "tokio",
+
 "tower 0.5.1",
 "tower-layer",
 "tower-service",
]
@@ -331,6 +364,26 @@ dependencies = [
]

[[package]]
+
name = "axum-core"
+
version = "0.4.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+
dependencies = [
+
 "async-trait",
+
 "bytes",
+
 "futures-util",
+
 "http 1.1.0",
+
 "http-body 1.0.1",
+
 "http-body-util",
+
 "mime",
+
 "pin-project-lite",
+
 "rustversion",
+
 "sync_wrapper 1.0.1",
+
 "tower-layer",
+
 "tower-service",
+
]
+

+
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1114,8 +1167,8 @@ dependencies = [
 "tonic",
 "tonic-health",
 "tonic-web",
-
 "tower",
-
 "tower-http",
+
 "tower 0.4.13",
+
 "tower-http 0.4.4",
 "tower-layer",
 "tracing",
 "tracing-core",
@@ -2164,6 +2217,7 @@ dependencies = [
 "http 1.1.0",
 "http-body 1.0.1",
 "httparse",
+
 "httpdate",
 "itoa 1.0.11",
 "pin-project-lite",
 "smallvec",
@@ -2538,6 +2592,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"

[[package]]
+
name = "lexopt"
+
version = "0.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "baff4b617f7df3d896f97fe922b64817f6cd9a756bb81d40f8883f2f66dcb401"
+

+
[[package]]
name = "libappindicator"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3992,7 +4052,6 @@ version = "0.0.0"
dependencies = [
 "anyhow",
 "base64 0.22.1",
-
 "localtime",
 "log",
 "radicle",
 "radicle-surf",
@@ -4014,6 +4073,9 @@ dependencies = [
name = "radicle-types"
version = "0.1.0"
dependencies = [
+
 "anyhow",
+
 "base64 0.22.1",
+
 "localtime",
 "radicle",
 "radicle-surf",
 "serde",
@@ -4507,6 +4569,16 @@ dependencies = [
]

[[package]]
+
name = "serde_path_to_error"
+
version = "0.1.16"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
+
dependencies = [
+
 "itoa 1.0.11",
+
 "serde",
+
]
+

+
[[package]]
name = "serde_repr"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5383,6 +5455,24 @@ dependencies = [
]

[[package]]
+
name = "test-http-api"
+
version = "0.1.0"
+
dependencies = [
+
 "anyhow",
+
 "axum 0.7.7",
+
 "hyper 1.5.0",
+
 "lexopt",
+
 "radicle",
+
 "radicle-surf",
+
 "radicle-types",
+
 "serde",
+
 "serde_json",
+
 "thiserror",
+
 "tokio",
+
 "tower-http 0.5.2",
+
]
+

+
[[package]]
name = "thin-slice"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5603,7 +5693,7 @@ checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e"
dependencies = [
 "async-stream",
 "async-trait",
-
 "axum",
+
 "axum 0.6.20",
 "base64 0.21.7",
 "bytes",
 "h2",
@@ -5616,7 +5706,7 @@ dependencies = [
 "prost",
 "tokio",
 "tokio-stream",
-
 "tower",
+
 "tower 0.4.13",
 "tower-layer",
 "tower-service",
 "tracing",
@@ -5649,7 +5739,7 @@ dependencies = [
 "pin-project",
 "tokio-stream",
 "tonic",
-
 "tower-http",
+
 "tower-http 0.4.4",
 "tower-layer",
 "tower-service",
 "tracing",
@@ -5676,6 +5766,21 @@ dependencies = [
]

[[package]]
+
name = "tower"
+
version = "0.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f"
+
dependencies = [
+
 "futures-core",
+
 "futures-util",
+
 "pin-project-lite",
+
 "sync_wrapper 0.1.2",
+
 "tokio",
+
 "tower-layer",
+
 "tower-service",
+
]
+

+
[[package]]
name = "tower-http"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5694,6 +5799,22 @@ dependencies = [
]

[[package]]
+
name = "tower-http"
+
version = "0.5.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
+
dependencies = [
+
 "bitflags 2.6.0",
+
 "bytes",
+
 "http 1.1.0",
+
 "http-body 1.0.1",
+
 "http-body-util",
+
 "pin-project-lite",
+
 "tower-layer",
+
 "tower-service",
+
]
+

+
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
modified Cargo.toml
@@ -3,5 +3,6 @@ resolver = "1"

members = [ 
    "crates/radicle-tauri",
-
    "crates/radicle-types"
+
    "crates/radicle-types",
+
    "crates/test-http-api",
]
modified crates/radicle-tauri/Cargo.toml
@@ -18,7 +18,6 @@ tauri-build = { version = "2.0.1", features = ["isolation"] }
anyhow = { version = "1.0.90" }
base64 = { version = "0.22.1" }
log = { version = "0.4.22" }
-
localtime = { version = "1.3.1" }
radicle = { git = "https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git" }
radicle-types = { version = "0.1.0", path = "../radicle-types" }
radicle-surf = { version = "0.22.1", features = ["serde"] }
modified crates/radicle-tauri/src/commands/auth.rs
@@ -1,28 +1,8 @@
-
use anyhow::anyhow;
-

-
use radicle::crypto::ssh;
+
use radicle_types::traits::auth::Auth;

use crate::{error::Error, AppState};

#[tauri::command]
-
pub fn authenticate(ctx: tauri::State<AppState>) -> Result<(), Error> {
-
    let profile = &ctx.profile;
-

-
    if !profile.keystore.is_encrypted()? {
-
        return Ok(());
-
    }
-
    match ssh::agent::Agent::connect() {
-
        Ok(mut agent) => {
-
            if agent.request_identities()?.contains(&profile.public_key) {
-
                Ok(())
-
            } else {
-
                Err(Error::WithHint {
-
                    err: anyhow!("Not able to find your keys in the ssh agent"),
-
                    hint: "Make sure to run <code>rad auth</code> in your terminal to add your keys to the ssh-agent.",
-
                })?
-
            }
-
        }
-
        Err(e) if e.is_not_running() => Err(Error::WithHint { err: anyhow!("SSH Agent is not running"), hint: "For now we require the user to have an ssh agent running, since we don't have passphrase inputs yet." })?, 
-
        Err(e) => Err(e)?,
-
    }
+
pub(crate) fn authenticate(ctx: tauri::State<AppState>) -> Result<(), Error> {
+
    ctx.authenticate().map_err(Error::from)
}
modified crates/radicle-tauri/src/commands/cob.rs
@@ -1,10 +1,9 @@
-
use base64::{engine::general_purpose::STANDARD, Engine as _};
-

use radicle::cob;
use radicle::git;
use radicle::identity;
-
use radicle::storage::{ReadRepository, ReadStorage};
use radicle_types as types;
+
use radicle_types::traits::cobs::Cobs;
+
use radicle_types::traits::thread::Thread;

use crate::{error, AppState};

@@ -18,10 +17,7 @@ pub async fn get_file_by_oid(
    rid: identity::RepoId,
    oid: git::Oid,
) -> Result<String, error::Error> {
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let blob = repo.blob(oid)?;
-

-
    Ok::<_, error::Error>(STANDARD.encode(blob.content()))
+
    ctx.get_embed(rid, oid).map_err(error::Error::from)
}

#[tauri::command]
@@ -31,10 +27,7 @@ pub async fn save_embed(
    name: &str,
    bytes: &[u8],
) -> Result<git::Oid, error::Error> {
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let embed = cob::Embed::<git::Oid>::store(name, bytes, &repo.backend)?;
-

-
    Ok::<_, error::Error>(embed.oid())
+
    ctx.save_embed(rid, name, bytes).map_err(error::Error::from)
}

#[tauri::command]
@@ -44,82 +37,6 @@ pub fn activity_by_id(
    type_name: cob::TypeName,
    id: git::Oid,
) -> Result<Vec<types::cobs::issue::Operation>, error::Error> {
-
    let aliases = ctx.profile.aliases();
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let ops = cob::store::ops(&id.into(), &type_name, &repo).unwrap();
-
    let mut actions: Vec<types::cobs::issue::Operation> = Vec::new();
-

-
    for op in ops.into_iter() {
-
        actions.extend(op.actions.iter().filter_map(
-
            |action: &Vec<u8>| -> Option<types::cobs::issue::Operation> {
-
                let action: types::cobs::issue::Action = serde_json::from_slice(action).ok()?;
-

-
                Some(types::cobs::issue::Operation {
-
                    entry_id: op.id,
-
                    action,
-
                    author: types::cobs::Author::new(op.author.into(), &aliases),
-
                    timestamp: op.timestamp,
-
                })
-
            },
-
        ))
-
    }
-

-
    Ok::<_, error::Error>(actions)
-
}
-

-
mod query {
-
    use serde::{Deserialize, Serialize};
-

-
    use radicle::issue;
-
    use radicle::patch;
-

-
    #[derive(Default, Serialize, Deserialize)]
-
    #[serde(rename_all = "camelCase")]
-
    pub enum IssueStatus {
-
        Closed,
-
        #[default]
-
        Open,
-
        All,
-
    }
-

-
    impl IssueStatus {
-
        pub fn matches(&self, issue: &issue::State) -> bool {
-
            match self {
-
                Self::Open => matches!(issue, issue::State::Open),
-
                Self::Closed => matches!(issue, issue::State::Closed { .. }),
-
                Self::All => true,
-
            }
-
        }
-
    }
-

-
    #[derive(Default, Serialize, Deserialize)]
-
    #[serde(rename_all = "camelCase")]
-
    pub enum PatchStatus {
-
        #[default]
-
        Open,
-
        Draft,
-
        Archived,
-
        Merged,
-
    }
-

-
    impl From<patch::Status> for PatchStatus {
-
        fn from(value: patch::Status) -> Self {
-
            match value {
-
                patch::Status::Archived => Self::Archived,
-
                patch::Status::Draft => Self::Draft,
-
                patch::Status::Merged => Self::Merged,
-
                patch::Status::Open => Self::Open,
-
            }
-
        }
-
    }
-
    impl From<PatchStatus> for patch::Status {
-
        fn from(value: PatchStatus) -> Self {
-
            match value {
-
                PatchStatus::Archived => Self::Archived,
-
                PatchStatus::Draft => Self::Draft,
-
                PatchStatus::Merged => Self::Merged,
-
                PatchStatus::Open => Self::Open,
-
            }
-
        }
-
    }
+
    ctx.activity_by_id(rid, type_name, id)
+
        .map_err(error::Error::from)
}
modified crates/radicle-tauri/src/commands/cob/draft.rs
@@ -1,10 +1,8 @@
use radicle::cob;
-
use radicle::cob::object::Storage;
use radicle::git;
-
use radicle::git::refs::storage::draft;
use radicle::identity;
-
use radicle::storage;
-
use radicle::storage::ReadStorage;
+

+
use radicle_types::traits::cobs::Cobs;

use crate::error::Error;
use crate::AppState;
@@ -20,26 +18,6 @@ pub fn publish_draft(
    cob_id: git::Oid,
    type_name: cob::TypeName,
) -> Result<(), Error> {
-
    let signer = ctx.profile.signer()?;
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let draft_oid =
-
        repo.backend
-
            .refname_to_id(&draft::cob(signer.public_key(), &type_name, &cob_id.into()))?;
-
    repo.update(
-
        signer.public_key(),
-
        &type_name,
-
        &cob_id.into(),
-
        &draft_oid.into(),
-
    )?;
-

-
    let mut patches = ctx.profile.patches_mut(&repo)?;
-
    patches.write(&cob_id.into())?;
-

-
    storage::git::cob::DraftStore::new(&repo, *signer.public_key()).remove(
-
        signer.public_key(),
-
        &type_name,
-
        &cob_id.into(),
-
    )?;
-

-
    Ok::<_, Error>(())
+
    ctx.publish_draft(rid, cob_id, type_name)
+
        .map_err(Error::from)
}
modified crates/radicle-tauri/src/commands/cob/issue.rs
@@ -1,12 +1,10 @@
use radicle::git;
use radicle::identity;
-
use radicle::issue::cache::Issues;
-
use radicle::node::Handle;
-
use radicle::node::Node;
-
use radicle::storage::ReadStorage;
+

use radicle_types as types;
+
use radicle_types::traits::issue::Issues;
+
use radicle_types::traits::issue::IssuesMut;

-
use crate::cob::query;
use crate::error::Error;
use crate::AppState;

@@ -17,25 +15,7 @@ pub fn create_issue(
    new: types::cobs::issue::NewIssue,
    opts: types::cobs::CobOptions,
) -> Result<types::cobs::issue::Issue, Error> {
-
    let mut node = Node::new(ctx.profile.socket());
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let signer = ctx.profile.signer()?;
-
    let aliases = ctx.profile.aliases();
-
    let mut issues = ctx.profile.issues_mut(&repo)?;
-
    let issue = issues.create(
-
        new.title,
-
        new.description,
-
        &new.labels,
-
        &new.assignees,
-
        new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
-
        &signer,
-
    )?;
-

-
    if opts.announce() {
-
        node.announce_refs(rid)?;
-
    }
-

-
    Ok::<_, Error>(types::cobs::issue::Issue::new(issue.id(), &issue, &aliases))
+
    ctx.create_issue(rid, new, opts).map_err(Error::from)
}

#[tauri::command]
@@ -46,103 +26,24 @@ pub fn edit_issue(
    action: types::cobs::issue::Action,
    opts: types::cobs::CobOptions,
) -> Result<types::cobs::issue::Issue, Error> {
-
    let mut node = Node::new(ctx.profile.socket());
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let signer = ctx.profile.signer()?;
-
    let aliases = ctx.profile.aliases();
-
    let mut issues = ctx.profile.issues_mut(&repo)?;
-
    let mut issue = issues.get_mut(&cob_id.into())?;
-

-
    match action {
-
        types::cobs::issue::Action::Lifecycle { state } => {
-
            issue.lifecycle(state.into(), &signer)?;
-
        }
-
        types::cobs::issue::Action::Assign { assignees } => {
-
            issue.assign(assignees, &signer)?;
-
        }
-
        types::cobs::issue::Action::Label { labels } => {
-
            issue.label(labels, &signer)?;
-
        }
-
        types::cobs::issue::Action::CommentReact {
-
            id,
-
            reaction,
-
            active,
-
        } => {
-
            issue.react(id, reaction, active, &signer)?;
-
        }
-
        types::cobs::issue::Action::CommentRedact { id } => {
-
            issue.redact_comment(id, &signer)?;
-
        }
-
        types::cobs::issue::Action::Comment {
-
            body,
-
            reply_to,
-
            embeds,
-
        } => {
-
            issue.comment(
-
                body,
-
                reply_to.unwrap_or(cob_id),
-
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
-
                &signer,
-
            )?;
-
        }
-
        types::cobs::issue::Action::CommentEdit { id, body, embeds } => {
-
            issue.edit_comment(
-
                id,
-
                body,
-
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
-
                &signer,
-
            )?;
-
        }
-
        types::cobs::issue::Action::Edit { title } => {
-
            issue.edit(title, &signer)?;
-
        }
-
    }
-

-
    if opts.announce() {
-
        node.announce_refs(rid)?;
-
    }
-

-
    Ok::<_, Error>(types::cobs::issue::Issue::new(issue.id(), &issue, &aliases))
+
    ctx.edit_issue(rid, cob_id, action, opts)
+
        .map_err(Error::from)
}

#[tauri::command]
-
pub fn list_issues(
+
pub(crate) fn list_issues(
    ctx: tauri::State<AppState>,
    rid: identity::RepoId,
-
    status: query::IssueStatus,
+
    status: Option<types::cobs::query::IssueStatus>,
) -> Result<Vec<types::cobs::issue::Issue>, Error> {
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let issues = ctx.profile.issues(&repo)?;
-
    let mut issues: Vec<_> = issues
-
        .list()?
-
        .filter_map(|r| {
-
            let (id, issue) = r.ok()?;
-
            (status.matches(issue.state())).then_some((id, issue))
-
        })
-
        .collect::<Vec<_>>();
-

-
    issues.sort_by(|(_, a), (_, b)| b.timestamp().cmp(&a.timestamp()));
-
    let aliases = &ctx.profile.aliases();
-
    let issues = issues
-
        .into_iter()
-
        .map(|(id, issue)| types::cobs::issue::Issue::new(&id, &issue, aliases))
-
        .collect::<Vec<_>>();
-

-
    Ok::<_, Error>(issues)
+
    ctx.list_issues(rid, status).map_err(Error::from)
}

#[tauri::command]
-
pub fn issue_by_id(
+
pub(crate) fn issue_by_id(
    ctx: tauri::State<AppState>,
    rid: identity::RepoId,
    id: git::Oid,
) -> Result<Option<types::cobs::issue::Issue>, Error> {
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let issues = ctx.profile.issues(&repo)?;
-
    let issue = issues.get(&id.into())?;
-

-
    let aliases = &ctx.profile.aliases();
-
    let issue = issue.map(|issue| types::cobs::issue::Issue::new(&id.into(), &issue, aliases));
-

-
    Ok::<_, Error>(issue)
+
    ctx.issue_by_id(rid, id).map_err(Error::from)
}
modified crates/radicle-tauri/src/commands/cob/patch.rs
@@ -1,15 +1,12 @@
use radicle::cob;
use radicle::git;
use radicle::identity;
-
use radicle::node::Handle;
-
use radicle::node::Node;
use radicle::patch;
-
use radicle::patch::cache::Patches;
-
use radicle::storage;
-
use radicle::storage::ReadStorage;
+

use radicle_types as types;
+
use radicle_types::traits::patch::Patches;
+
use radicle_types::traits::patch::PatchesMut;

-
use crate::cob::query;
use crate::error::Error;
use crate::AppState;

@@ -17,38 +14,12 @@ use crate::AppState;
pub async fn list_patches(
    ctx: tauri::State<'_, AppState>,
    rid: identity::RepoId,
-
    status: Option<query::PatchStatus>,
+
    status: Option<types::cobs::query::PatchStatus>,
    skip: Option<usize>,
    take: Option<usize>,
) -> Result<types::cobs::PaginatedQuery<Vec<types::cobs::patch::Patch>>, Error> {
-
    let cursor = skip.unwrap_or(0);
-
    let take = take.unwrap_or(20);
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let aliases = &ctx.profile.aliases();
-
    let cache = ctx.profile.patches(&repo)?;
-
    let patches = match status {
-
        None => cache.list()?.collect::<Vec<_>>(),
-
        Some(s) => cache.list_by_status(&s.into())?.collect::<Vec<_>>(),
-
    };
-
    let more = cursor + take < patches.len();
-

-
    let mut patches = patches
-
        .into_iter()
-
        .filter_map(|p| {
-
            p.map(|(id, patch)| types::cobs::patch::Patch::new(id, &patch, aliases))
-
                .ok()
-
        })
-
        .skip(cursor)
-
        .take(take)
-
        .collect::<Vec<_>>();
-

-
    patches.sort_by_key(|b| std::cmp::Reverse(b.timestamp()));
-

-
    Ok::<_, Error>(types::cobs::PaginatedQuery {
-
        cursor,
-
        more,
-
        content: patches,
-
    })
+
    ctx.list_patches(rid, status, skip, take)
+
        .map_err(Error::from)
}

#[tauri::command]
@@ -57,13 +28,7 @@ pub fn patch_by_id(
    rid: identity::RepoId,
    id: git::Oid,
) -> Result<Option<types::cobs::patch::Patch>, Error> {
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let patches = ctx.profile.patches(&repo)?;
-
    let patch = patches.get(&id.into())?;
-
    let aliases = &ctx.profile.aliases();
-
    let patches = patch.map(|patch| types::cobs::patch::Patch::new(id.into(), &patch, aliases));
-

-
    Ok::<_, Error>(patches)
+
    ctx.get_patch(rid, id).map_err(Error::from)
}

#[tauri::command]
@@ -72,18 +37,7 @@ pub fn revisions_by_patch(
    rid: identity::RepoId,
    id: git::Oid,
) -> Result<Option<Vec<types::cobs::patch::Revision>>, Error> {
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let patches = ctx.profile.patches(&repo)?;
-
    let revisions = patches.get(&id.into())?.map(|patch| {
-
        let aliases = &ctx.profile.aliases();
-

-
        patch
-
            .revisions()
-
            .map(|(_, r)| types::cobs::patch::Revision::new(r.clone(), aliases))
-
            .collect::<Vec<_>>()
-
    });
-

-
    Ok::<_, Error>(revisions)
+
    ctx.revisions_by_patch(rid, id).map_err(Error::from)
}

#[tauri::command]
@@ -93,23 +47,10 @@ pub fn revision_by_patch_and_id(
    id: git::Oid,
    revision_id: git::Oid,
) -> Result<Option<types::cobs::patch::Revision>, Error> {
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let patches = ctx.profile.patches(&repo)?;
-
    let revision = patches.get(&id.into())?.and_then(|patch| {
-
        let aliases = &ctx.profile.aliases();
-

-
        patch
-
            .revision(&revision_id.into())
-
            .map(|r| types::cobs::patch::Revision::new(r.clone(), aliases))
-
    });
-

-
    Ok::<_, Error>(revision)
+
    ctx.revision_by_id(rid, id, revision_id)
+
        .map_err(Error::from)
}

-
/// Creates a draft review for a specific patch revision.
-
///
-
/// This Tauri command allows users to create a new draft review for a specific patch revision.
-
/// The draft is associated with the user (signer) and the provided patch revision within the repository.
#[tauri::command]
pub fn create_draft_review(
    ctx: tauri::State<AppState>,
@@ -118,37 +59,8 @@ pub fn create_draft_review(
    cob_id: git::Oid,
    labels: Vec<cob::Label>,
) -> Result<patch::ReviewId, Error> {
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let signer = ctx.profile.signer()?;
-
    let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
-

-
    let mut patches = cob::patch::Cache::no_cache(&drafts)?;
-
    let mut patch = patches.get_mut(&cob_id.into())?;
-
    let revision = patch
-
        .revision(&revision_id)
-
        .ok_or_else(|| Error::WithHint {
-
            err: anyhow::anyhow!("patch revision not found"),
-
            hint: "Not able to find the specified patch revision.",
-
        })?;
-

-
    revision
-
        .review_by(signer.public_key())
-
        .ok_or(Error::WithHint {
-
            err: anyhow::anyhow!("duplicate patch review found"),
-
            hint: "Found an existing draft patch review on this patch revision and repo.",
-
        })?;
-

-
    let review_id = patch.review(
-
        revision.id(),
-
        Some(cob::patch::Verdict::Reject),
-
        None,
-
        labels,
-
        &signer,
-
    )?;
-

-
    patches.write(&cob_id.into())?;
-

-
    Ok::<_, Error>(review_id)
+
    ctx.create_draft_review(rid, revision_id, cob_id, labels)
+
        .map_err(Error::from)
}

/// Creates a new review comment on a draft review for a specific patch.
@@ -163,28 +75,8 @@ pub fn create_draft_review_comment(
    cob_id: git::Oid,
    new: types::cobs::thread::CreateReviewComment,
) -> Result<(), Error> {
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let signer = ctx.profile.signer()?;
-
    let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
-

-
    let mut patches = cob::patch::Cache::no_cache(&drafts)?;
-
    let mut patch = patches.get_mut(&cob_id.into())?;
-

-
    patch.transaction("Review comments", &signer, |tx| {
-
        tx.review_comment(
-
            new.review_id,
-
            new.body,
-
            new.location.map(|l| l.into()),
-
            new.reply_to,
-
            new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
-
        )?;
-

-
        Ok(())
-
    })?;
-

-
    patches.write(&cob_id.into())?;
-

-
    Ok::<_, Error>(())
+
    ctx.create_draft_review_comment(rid, cob_id, new)
+
        .map_err(Error::from)
}

/// Edits a draft review for a specific patch revision in a repository.
@@ -198,31 +90,10 @@ pub fn edit_draft_review(
    cob_id: git::Oid,
    edit: types::cobs::patch::ReviewEdit,
) -> Result<(), Error> {
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let signer = ctx.profile.signer()?;
-
    let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
-

-
    let mut patches = cob::patch::Cache::no_cache(&drafts)?;
-
    let mut patch = patches.get_mut(&cob_id.into())?;
-
    patch.review_edit(
-
        edit.review_id,
-
        edit.verdict,
-
        edit.summary,
-
        edit.labels,
-
        &signer,
-
    )?;
-

-
    patches.write(&cob_id.into())?;
-

-
    Ok::<_, Error>(())
+
    ctx.edit_draft_review(rid, cob_id, edit)
+
        .map_err(Error::from)
}

-
/// Gets the draft review of the local user for a specific patch revision in a repository.
-
///
-
/// This Tauri command is used to retrieve a patch review draft for the local user
-
/// on a given patch revision from a repository.
-
/// It looks up the repository using the provided repository ID (`rid`) and patch object ID (`cob_id`),
-
/// and gets the patch review of the local user associated with a specific revision (`revision_id`), if it exists.
#[tauri::command]
pub fn get_draft_review(
    ctx: tauri::State<AppState>,
@@ -230,16 +101,7 @@ pub fn get_draft_review(
    cob_id: git::Oid,
    revision_id: patch::RevisionId,
) -> Option<patch::Review> {
-
    let repo = ctx.profile.storage.repository(rid).ok()?;
-
    let signer = ctx.profile.signer().ok()?;
-
    let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
-
    let patches = cob::patch::Cache::no_cache(&drafts).ok()?;
-

-
    let patch = patches.get(&cob_id.into()).ok()?;
-
    let revision = patch.and_then(|p| p.revision(&revision_id).cloned());
-
    let review = revision.and_then(|rev| rev.review_by(signer.public_key()).cloned());
-

-
    review
+
    ctx.get_draft_review(rid, cob_id, revision_id)
}

#[tauri::command]
@@ -250,177 +112,6 @@ pub fn edit_patch(
    action: types::cobs::patch::Action,
    opts: types::cobs::CobOptions,
) -> Result<types::cobs::patch::Patch, Error> {
-
    let mut node = Node::new(ctx.profile.socket());
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let signer = ctx.profile.signer()?;
-
    let aliases = ctx.profile.aliases();
-
    let mut patches = ctx.profile.patches_mut(&repo)?;
-
    let mut patch = patches.get_mut(&cob_id.into())?;
-

-
    match action {
-
        types::cobs::patch::Action::RevisionEdit {
-
            revision,
-
            description,
-
            embeds,
-
        } => {
-
            patch.edit_revision(
-
                revision,
-
                description,
-
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
-
                &signer,
-
            )?;
-
        }
-
        types::cobs::patch::Action::RevisionCommentRedact { revision, comment } => {
-
            patch.comment_redact(revision, comment, &signer)?;
-
        }
-
        types::cobs::patch::Action::ReviewCommentRedact { review, comment } => {
-
            patch.redact_review_comment(review, comment, &signer)?;
-
        }
-
        types::cobs::patch::Action::ReviewCommentReact {
-
            review,
-
            comment,
-
            reaction,
-
            active,
-
        } => {
-
            patch.react_review_comment(review, comment, reaction, active, &signer)?;
-
        }
-
        types::cobs::patch::Action::ReviewCommentResolve { review, comment } => {
-
            patch.resolve_review_comment(review, comment, &signer)?;
-
        }
-
        types::cobs::patch::Action::ReviewCommentUnresolve { review, comment } => {
-
            patch.unresolve_review_comment(review, comment, &signer)?;
-
        }
-
        types::cobs::patch::Action::Edit { title, target } => {
-
            patch.edit(title, target, &signer)?;
-
        }
-
        types::cobs::patch::Action::ReviewEdit {
-
            review,
-
            summary,
-
            verdict,
-
            labels,
-
        } => {
-
            patch.review_edit(review, verdict, summary, labels, &signer)?;
-
        }
-
        types::cobs::patch::Action::Review {
-
            revision,
-
            summary,
-
            verdict,
-
            labels,
-
        } => {
-
            patch.review(revision, verdict, summary, labels, &signer)?;
-
        }
-
        types::cobs::patch::Action::ReviewRedact { review } => {
-
            patch.redact_review(review, &signer)?;
-
        }
-
        types::cobs::patch::Action::ReviewComment {
-
            review,
-
            body,
-
            location,
-
            reply_to,
-
            embeds,
-
        } => {
-
            patch.review_comment(
-
                review,
-
                body,
-
                location.map(|l| l.into()),
-
                reply_to,
-
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
-
                &signer,
-
            )?;
-
        }
-
        types::cobs::patch::Action::ReviewCommentEdit {
-
            review,
-
            comment,
-
            body,
-
            embeds,
-
        } => {
-
            patch.edit_review_comment(
-
                review,
-
                comment,
-
                body,
-
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
-
                &signer,
-
            )?;
-
        }
-
        types::cobs::patch::Action::Lifecycle { state } => {
-
            patch.lifecycle(state, &signer)?;
-
        }
-
        types::cobs::patch::Action::Assign { assignees } => {
-
            patch.assign(assignees, &signer)?;
-
        }
-
        types::cobs::patch::Action::Label { labels } => {
-
            patch.label(labels, &signer)?;
-
        }
-
        types::cobs::patch::Action::RevisionReact {
-
            revision,
-
            reaction,
-
            location,
-
            active,
-
        } => {
-
            patch.react(
-
                revision,
-
                reaction,
-
                location.map(|l| l.into()),
-
                active,
-
                &signer,
-
            )?;
-
        }
-
        types::cobs::patch::Action::RevisionComment {
-
            revision,
-
            location,
-
            body,
-
            reply_to,
-
            embeds,
-
        } => {
-
            patch.comment(
-
                revision,
-
                body,
-
                reply_to,
-
                location.map(|l| l.into()),
-
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
-
                &signer,
-
            )?;
-
        }
-
        types::cobs::patch::Action::RevisionCommentEdit {
-
            revision,
-
            comment,
-
            body,
-
            embeds,
-
        } => {
-
            patch.comment_edit(
-
                revision,
-
                comment,
-
                body,
-
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
-
                &signer,
-
            )?;
-
        }
-
        types::cobs::patch::Action::RevisionCommentReact {
-
            revision,
-
            comment,
-
            reaction,
-
            active,
-
        } => {
-
            patch.comment_react(revision, comment, reaction, active, &signer)?;
-
        }
-
        types::cobs::patch::Action::RevisionRedact { revision } => {
-
            patch.redact(revision, &signer)?;
-
        }
-
        types::cobs::patch::Action::Merge { .. } => {
-
            unimplemented!("We don't support merging of patches through the desktop")
-
        }
-
        types::cobs::patch::Action::Revision { .. } => {
-
            unimplemented!("We don't support creating new revisions through the desktop")
-
        }
-
    }
-

-
    if opts.announce() {
-
        node.announce_refs(rid)?;
-
    }
-

-
    Ok::<_, Error>(types::cobs::patch::Patch::new(
-
        *patch.id(),
-
        &patch,
-
        &aliases,
-
    ))
+
    ctx.edit_patch(rid, cob_id, action, opts)
+
        .map_err(Error::from)
}
modified crates/radicle-tauri/src/commands/diff.rs
@@ -1,45 +1,15 @@
-
use radicle::storage::ReadStorage;
use radicle_surf as surf;

-
use radicle::git;
use radicle::identity;
-
use serde::Deserialize;
-
use serde::Serialize;
+
use radicle_types::traits::repo::Repo;

use crate::{error, AppState};

-
#[derive(Serialize, Deserialize)]
-
pub struct Options {
-
    pub base: git::Oid,
-
    pub head: git::Oid,
-
    pub unified: u32,
-
}
-

#[tauri::command]
pub async fn get_diff(
    ctx: tauri::State<'_, AppState>,
    rid: identity::RepoId,
-
    options: Options,
+
    options: radicle_types::cobs::diff::Options,
) -> Result<surf::diff::Diff, error::Error> {
-
    let repo = ctx.profile.storage.repository(rid)?.backend;
-
    let base = repo.find_commit(*options.base)?;
-
    let head = repo.find_commit(*options.head)?;
-

-
    let mut opts = git::raw::DiffOptions::new();
-
    opts.patience(true)
-
        .minimal(true)
-
        .context_lines(options.unified);
-

-
    let mut find_opts = git::raw::DiffFindOptions::new();
-
    find_opts.exact_match_only(true);
-
    find_opts.all(true);
-

-
    let left = base.tree()?;
-
    let right = head.tree()?;
-

-
    let mut diff = repo.diff_tree_to_tree(Some(&left), Some(&right), Some(&mut opts))?;
-
    diff.find_similar(Some(&mut find_opts))?;
-
    let diff = surf::diff::Diff::try_from(diff)?;
-

-
    Ok::<_, error::Error>(diff)
+
    ctx.get_diff(rid, options).map_err(error::Error::from)
}
modified crates/radicle-tauri/src/commands/profile.rs
@@ -1,15 +1,9 @@
use radicle_types::config::Config;
+
use radicle_types::traits::Profile;

-
use crate::error::Error;
use crate::AppState;

#[tauri::command]
-
pub fn config(ctx: tauri::State<AppState>) -> Result<Config, Error> {
-
    let config = Config {
-
        public_key: ctx.profile.public_key,
-
        alias: ctx.profile.config.node.alias.clone(),
-
        seeding_policy: ctx.profile.config.node.seeding_policy,
-
    };
-

-
    Ok::<_, Error>(config)
+
pub fn config(ctx: tauri::State<AppState>) -> Config {
+
    ctx.config()
}
modified crates/radicle-tauri/src/commands/repo.rs
@@ -1,40 +1,15 @@
-
use radicle::crypto::Verified;
-
use radicle::identity::doc::PayloadId;
-
use radicle::identity::{DocAt, RepoId};
-
use radicle::issue::cache::Issues;
-
use radicle::node::routing::Store;
-
use radicle::patch::cache::Patches;
-
use radicle::prelude::Doc;
-
use radicle::storage::git::Repository;
-
use radicle::storage::ReadStorage;
-
use radicle::storage::{self, ReadRepository};
+
use radicle::git;
+
use radicle::identity::RepoId;
+

use radicle_types as types;
+
use radicle_types::traits::repo::Repo;

use crate::error::Error;
use crate::AppState;

#[tauri::command]
pub fn list_repos(ctx: tauri::State<AppState>) -> Result<Vec<types::repo::RepoInfo>, Error> {
-
    let storage = &ctx.profile.storage;
-
    let policies = ctx.profile.policies()?;
-
    let mut repos = storage.repositories()?.into_iter().collect::<Vec<_>>();
-
    repos.sort_by_key(|p| p.rid);
-

-
    let infos = repos
-
        .into_iter()
-
        .filter_map(|info| {
-
            if !policies.is_seeding(&info.rid).unwrap_or_default() {
-
                return None;
-
            }
-
            let repo = ctx.profile.storage.repository(info.rid).ok()?;
-
            let DocAt { doc, .. } = repo.identity_doc().ok()?;
-
            let repo_info = repo_info(&ctx.profile, &repo, &doc).ok()?;
-

-
            Some(repo_info)
-
        })
-
        .collect::<Vec<_>>();
-

-
    Ok::<_, Error>(infos)
+
    ctx.list_repos().map_err(Error::from)
}

#[tauri::command]
@@ -42,72 +17,15 @@ pub fn repo_by_id(
    ctx: tauri::State<AppState>,
    rid: RepoId,
) -> Result<types::repo::RepoInfo, Error> {
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let DocAt { doc, .. } = repo.identity_doc()?;
-

-
    let repo_info = repo_info(&ctx.profile, &repo, &doc)?;
-

-
    Ok::<_, Error>(repo_info)
+
    ctx.repo_by_id(rid).map_err(Error::from)
}

#[tauri::command]
pub async fn diff_stats(
    ctx: tauri::State<'_, AppState>,
    rid: RepoId,
-
    base: String,
-
    head: String,
+
    base: git::Oid,
+
    head: git::Oid,
) -> Result<types::cobs::Stats, Error> {
-
    let repo = radicle_surf::Repository::open(storage::git::paths::repository(
-
        &ctx.profile.storage,
-
        &rid,
-
    ))?;
-
    let base = repo.commit(base)?;
-
    let commit = repo.commit(head)?;
-
    let diff = repo.diff(base.id, commit.id)?;
-
    let stats = diff.stats();
-

-
    Ok::<_, Error>(types::cobs::Stats::new(stats))
-
}
-

-
pub fn repo_info(
-
    profile: &radicle::Profile,
-
    repo: &Repository,
-
    doc: &Doc<Verified>,
-
) -> Result<types::repo::RepoInfo, Error> {
-
    let aliases = profile.aliases();
-
    let delegates = doc
-
        .delegates
-
        .clone()
-
        .into_iter()
-
        .map(|did| types::cobs::Author::new(did, &aliases))
-
        .collect::<Vec<_>>();
-
    let db = profile.database()?;
-
    let seeding = db.count(&repo.id).unwrap_or_default();
-
    let project = doc.payload.get(&PayloadId::project()).and_then(|payload| {
-
        let (_, head) = repo.head().ok()?;
-
        let commit = repo.commit(head).ok()?;
-
        let patches = profile.patches(repo).ok()?;
-
        let patches = patches.counts().ok()?;
-
        let issues = profile.issues(repo).ok()?;
-
        let issues = issues.counts().ok()?;
-

-
        let data: types::repo::ProjectPayloadData = (*payload).clone().try_into().ok()?;
-
        let meta = types::repo::ProjectPayloadMeta {
-
            issues,
-
            patches,
-
            head,
-
            last_commit_timestamp: commit.time().seconds() * 1000,
-
        };
-

-
        Some(types::repo::ProjectPayload::new(data, meta))
-
    });
-

-
    Ok::<_, Error>(types::repo::RepoInfo {
-
        payloads: types::repo::SupportedPayloads { project },
-
        delegates,
-
        threshold: doc.threshold,
-
        visibility: doc.visibility.clone().into(),
-
        rid: repo.id,
-
        seeding,
-
    })
+
    ctx.diff_stats(rid, base, head).map_err(Error::from)
}
modified crates/radicle-tauri/src/commands/thread.rs
@@ -1,11 +1,7 @@
-
use localtime::LocalTime;
-

-
use radicle::cob;
use radicle::identity;
-
use radicle::node::Handle;
-
use radicle::storage::ReadStorage;
-
use radicle::Node;
+

use radicle_types as types;
+
use radicle_types::traits::thread::Thread;

use crate::error::Error;
use crate::AppState;
@@ -17,40 +13,8 @@ pub fn create_issue_comment(
    new: types::cobs::thread::NewIssueComment,
    opts: types::cobs::CobOptions,
) -> Result<types::cobs::thread::Comment<types::cobs::Never>, Error> {
-
    let aliases = &ctx.profile.aliases();
-
    let mut node = Node::new(ctx.profile.socket());
-
    let signer = ctx.profile.signer()?;
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let mut issues = ctx.profile.issues_mut(&repo)?;
-
    let mut issue = issues.get_mut(&new.id.into())?;
-
    let id = new.reply_to.unwrap_or_else(|| {
-
        let (root_id, _) = issue.root();
-
        *root_id
-
    });
-
    let n = new.clone();
-
    let oid = issue.comment(
-
        n.body,
-
        id,
-
        n.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
-
        &signer,
-
    )?;
-

-
    if opts.announce() {
-
        node.announce_refs(rid)?;
-
    }
-

-
    Ok(types::cobs::thread::Comment::<types::cobs::Never>::new(
-
        oid,
-
        cob::thread::Comment::new(
-
            *signer.public_key(),
-
            new.body,
-
            id.into(),
-
            None,
-
            new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
-
            LocalTime::now().into(),
-
        ),
-
        aliases,
-
    ))
+
    ctx.create_issue_comment(rid, new, opts)
+
        .map_err(Error::from)
}

#[tauri::command]
@@ -60,38 +24,6 @@ pub fn create_patch_comment(
    new: types::cobs::thread::NewPatchComment,
    opts: types::cobs::CobOptions,
) -> Result<types::cobs::thread::Comment<types::cobs::thread::CodeLocation>, Error> {
-
    let aliases = &ctx.profile.aliases();
-
    let mut node = Node::new(ctx.profile.socket());
-
    let signer = ctx.profile.signer()?;
-
    let repo = ctx.profile.storage.repository(rid)?;
-
    let mut patches = ctx.profile.patches_mut(&repo)?;
-
    let mut patch = patches.get_mut(&new.id.into())?;
-
    let n = new.clone();
-
    let oid = patch.comment(
-
        new.revision.into(),
-
        n.body,
-
        n.reply_to,
-
        n.location.map(|l| l.into()),
-
        n.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
-
        &signer,
-
    )?;
-

-
    if opts.announce() {
-
        node.announce_refs(rid)?;
-
    }
-

-
    Ok(types::cobs::thread::Comment::<
-
        types::cobs::thread::CodeLocation,
-
    >::new(
-
        oid,
-
        cob::thread::Comment::new(
-
            *signer.public_key(),
-
            new.body,
-
            new.reply_to,
-
            new.location.map(|l| l.into()),
-
            new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
-
            LocalTime::now().into(),
-
        ),
-
        aliases,
-
    ))
+
    ctx.create_patch_comment(rid, new, opts)
+
        .map_err(Error::from)
}
modified crates/radicle-tauri/src/error.rs
@@ -51,6 +51,10 @@ pub enum Error {
    #[error(transparent)]
    Storage(#[from] radicle::storage::Error),

+
    /// Types error.
+
    #[error(transparent)]
+
    Types(#[from] radicle_types::error::Error),
+

    /// Surf error.
    #[error(transparent)]
    Surf(#[from] radicle_surf::Error),
modified crates/radicle-tauri/src/lib.rs
@@ -6,6 +6,13 @@ use tauri::Manager;

use radicle::node::Handle;
use radicle::Node;
+
use radicle_types::traits::auth::Auth;
+
use radicle_types::traits::cobs::Cobs;
+
use radicle_types::traits::issue::{Issues, IssuesMut};
+
use radicle_types::traits::patch::{Patches, PatchesMut};
+
use radicle_types::traits::repo::Repo;
+
use radicle_types::traits::thread::Thread;
+
use radicle_types::traits::Profile;

use commands::{auth, cob, diff, profile, repo, thread};

@@ -13,6 +20,20 @@ struct AppState {
    profile: radicle::Profile,
}

+
impl Auth for AppState {}
+
impl Repo for AppState {}
+
impl Thread for AppState {}
+
impl Cobs for AppState {}
+
impl Issues for AppState {}
+
impl IssuesMut for AppState {}
+
impl Patches for AppState {}
+
impl PatchesMut for AppState {}
+
impl Profile for AppState {
+
    fn profile(&self) -> radicle::Profile {
+
        self.profile.clone()
+
    }
+
}
+

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    #[cfg(debug_assertions)]
modified crates/radicle-types/Cargo.toml
@@ -4,9 +4,12 @@ version = "0.1.0"
edition = "2021"

[dependencies]
+
anyhow = { version = "1.0.90" }
+
base64 = { version = "0.22.1" }
radicle = { git = "https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git" }
radicle-surf = { version = "0.22.1", features = ["serde"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = { version = "1.0.132" }
thiserror = { version = "1.0.65" }
-
ts-rs = { version = "10.0.0", features = ["serde-json-impl", "no-serde-warnings"] }
+
ts-rs = { version = "10.0.0", features = [ "serde-json-impl", "no-serde-warnings" ] }
+
localtime = { version = "1.3.1" }
modified crates/radicle-types/src/cobs.rs
@@ -4,11 +4,12 @@ use ts_rs::TS;
use radicle::identity;
use radicle::node::{Alias, AliasStore};

+
pub mod diff;
pub mod issue;
pub mod patch;
pub mod thread;

-
#[derive(Serialize, TS)]
+
#[derive(Debug, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/")]
@@ -78,3 +79,60 @@ impl Stats {
        }
    }
}
+

+
pub mod query {
+
    use serde::{Deserialize, Serialize};
+

+
    use radicle::issue;
+
    use radicle::patch;
+

+
    #[derive(Default, Serialize, Deserialize)]
+
    #[serde(rename_all = "camelCase")]
+
    pub enum IssueStatus {
+
        Closed,
+
        #[default]
+
        Open,
+
        All,
+
    }
+

+
    impl IssueStatus {
+
        pub fn matches(&self, issue: &issue::State) -> bool {
+
            match self {
+
                Self::Open => matches!(issue, issue::State::Open),
+
                Self::Closed => matches!(issue, issue::State::Closed { .. }),
+
                Self::All => true,
+
            }
+
        }
+
    }
+

+
    #[derive(Default, Serialize, Deserialize, Clone)]
+
    #[serde(rename_all = "camelCase")]
+
    pub enum PatchStatus {
+
        #[default]
+
        Open,
+
        Draft,
+
        Archived,
+
        Merged,
+
    }
+

+
    impl From<patch::Status> for PatchStatus {
+
        fn from(value: patch::Status) -> Self {
+
            match value {
+
                patch::Status::Archived => Self::Archived,
+
                patch::Status::Draft => Self::Draft,
+
                patch::Status::Merged => Self::Merged,
+
                patch::Status::Open => Self::Open,
+
            }
+
        }
+
    }
+
    impl From<PatchStatus> for patch::Status {
+
        fn from(value: PatchStatus) -> Self {
+
            match value {
+
                PatchStatus::Archived => Self::Archived,
+
                PatchStatus::Draft => Self::Draft,
+
                PatchStatus::Merged => Self::Merged,
+
                PatchStatus::Open => Self::Open,
+
            }
+
        }
+
    }
+
}
added crates/radicle-types/src/cobs/diff.rs
@@ -0,0 +1,9 @@
+
use radicle::git;
+
use serde::{Deserialize, Serialize};
+

+
#[derive(Serialize, Deserialize)]
+
pub struct Options {
+
    pub base: git::Oid,
+
    pub head: git::Oid,
+
    pub unified: u32,
+
}
modified crates/radicle-types/src/cobs/patch.rs
@@ -12,7 +12,7 @@ use radicle::patch;

use crate::cobs;

-
#[derive(TS, Serialize)]
+
#[derive(Debug, TS, Serialize)]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
#[serde(rename_all = "camelCase")]
@@ -57,7 +57,7 @@ impl Patch {
    }
}

-
#[derive(Serialize, Deserialize, TS)]
+
#[derive(Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", tag = "status")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
modified crates/radicle-types/src/error.rs
@@ -2,12 +2,89 @@ use serde::Serialize;

#[derive(Debug, thiserror::Error)]
pub enum Error {
+
    /// Profile error.
+
    #[error(transparent)]
+
    Profile(#[from] radicle::profile::Error),
+

+
    /// Crypto error.
+
    #[error(transparent)]
+
    Crypto(#[from] radicle::crypto::ssh::keystore::Error),
+

+
    /// SSH Agent error.
+
    #[error(transparent)]
+
    Agent(#[from] radicle::crypto::ssh::agent::Error),
+

+
    /// Node database error.
+
    #[error(transparent)]
+
    Database(#[from] radicle::node::db::Error),
+

+
    /// Repository error.
+
    #[error(transparent)]
+
    Repository(#[from] radicle::storage::RepositoryError),
+

+
    /// Policy store error.
+
    #[error(transparent)]
+
    PolicyStore(#[from] radicle::node::policy::store::Error),
+

+
    /// Cob patch cache error.
+
    #[error(transparent)]
+
    CachePatch(#[from] radicle::cob::patch::cache::Error),
+

+
    /// Diff error.
+
    #[error(transparent)]
+
    Diff(#[from] radicle_surf::diff::git::error::Diff),
+

+
    /// Storage error.
+
    #[error(transparent)]
+
    Storage(#[from] radicle::storage::Error),
+

+
    /// Radicle Git error.
+
    #[error(transparent)]
+
    Git(#[from] radicle::git::Error),
+

+
    /// Surf error.
+
    #[error(transparent)]
+
    Surf(#[from] radicle_surf::Error),
+

+
    /// Git2 error.
+
    #[error(transparent)]
+
    Git2(#[from] radicle::git::raw::Error),
+

+
    /// Cob issue cache error.
+
    #[error(transparent)]
+
    CacheIssue(#[from] radicle::cob::issue::cache::Error),
+

+
    /// Patch error.
+
    #[error(transparent)]
+
    Patch(#[from] radicle::patch::Error),
+

+
    /// Issue error.
+
    #[error(transparent)]
+
    Issue(#[from] radicle::issue::Error),
+

+
    /// Node error.
+
    #[error(transparent)]
+
    Node(#[from] radicle::node::Error),
+

+
    /// An error with a hint.
+
    #[error("{err} {hint}")]
+
    WithHint {
+
        err: anyhow::Error,
+
        hint: &'static str,
+
    },
+

    /// Serde JSON error.
    #[error(transparent)]
    SerdeJSON(#[from] serde_json::error::Error),
}

#[derive(Serialize)]
+
struct ErrorWrapperWithHint {
+
    err: String,
+
    hint: String,
+
}
+

+
#[derive(Serialize)]
struct ErrorWrapper {
    err: String,
}
@@ -17,6 +94,15 @@ impl Serialize for Error {
    where
        S: serde::ser::Serializer,
    {
+
        if let Error::WithHint { err, hint } = self {
+
            let error_wrapper = ErrorWrapperWithHint {
+
                err: err.to_string(),
+
                hint: hint.to_string(),
+
            };
+

+
            return error_wrapper.serialize(serializer);
+
        }
+

        let wrapper = ErrorWrapper {
            err: self.to_string(),
        };
modified crates/radicle-types/src/lib.rs
@@ -2,3 +2,4 @@ pub mod cobs;
pub mod config;
pub mod error;
pub mod repo;
+
pub mod traits;
added crates/radicle-types/src/traits.rs
@@ -0,0 +1,22 @@
+
use crate::config::Config;
+

+
pub mod auth;
+
pub mod cobs;
+
pub mod issue;
+
pub mod patch;
+
pub mod repo;
+
pub mod thread;
+

+
pub trait Profile {
+
    fn profile(&self) -> radicle::Profile;
+

+
    fn config(&self) -> Config {
+
        let p = self.profile();
+

+
        Config {
+
            public_key: p.public_key,
+
            alias: p.config.node.alias.clone(),
+
            seeding_policy: p.config.node.seeding_policy,
+
        }
+
    }
+
}
added crates/radicle-types/src/traits/auth.rs
@@ -0,0 +1,31 @@
+
use anyhow::anyhow;
+

+
use radicle::crypto::ssh;
+

+
use crate::error::Error;
+

+
use super::Profile;
+

+
pub trait Auth: Profile {
+
    fn authenticate(&self) -> Result<(), Error> {
+
        let profile = &self.profile();
+

+
        if !profile.keystore.is_encrypted()? {
+
            return Ok(());
+
        }
+
        match ssh::agent::Agent::connect() {
+
        Ok(mut agent) => {
+
            if agent.request_identities()?.contains(&profile.public_key) {
+
                Ok(())
+
            } else {
+
                Err(Error::WithHint {
+
                    err: anyhow!("Not able to find your keys in the ssh agent"),
+
                    hint: "Make sure to run <code>rad auth</code> in your terminal to add your keys to the ssh-agent.",
+
                })?
+
            }
+
        }
+
        Err(e) if e.is_not_running() => Err(Error::WithHint { err: anyhow!("SSH Agent is not running"), hint: "For now we require the user to have an ssh agent running, since we don't have passphrase inputs yet." })?, 
+
        Err(e) => Err(e)?,
+
    }
+
    }
+
}
added crates/radicle-types/src/traits/cobs.rs
@@ -0,0 +1,72 @@
+
use radicle::cob::object::Storage;
+
use radicle::storage::refs::draft;
+
use radicle::storage::{self, ReadStorage};
+
use radicle::{cob, git, identity};
+

+
use crate::error::Error;
+
use crate::traits::Profile;
+

+
pub trait Cobs: Profile {
+
    fn activity_by_id(
+
        &self,
+
        rid: identity::RepoId,
+
        type_name: cob::TypeName,
+
        id: git::Oid,
+
    ) -> Result<Vec<crate::cobs::issue::Operation>, Error> {
+
        let profile = self.profile();
+
        let aliases = profile.aliases();
+
        let repo = profile.storage.repository(rid)?;
+
        let ops = cob::store::ops(&id.into(), &type_name, &repo).unwrap();
+
        let mut actions: Vec<crate::cobs::issue::Operation> = Vec::new();
+

+
        for op in ops.into_iter() {
+
            actions.extend(op.actions.iter().filter_map(
+
                |action: &Vec<u8>| -> Option<crate::cobs::issue::Operation> {
+
                    let action: crate::cobs::issue::Action = serde_json::from_slice(action).ok()?;
+

+
                    Some(crate::cobs::issue::Operation {
+
                        entry_id: op.id,
+
                        action,
+
                        author: crate::cobs::Author::new(op.author.into(), &aliases),
+
                        timestamp: op.timestamp,
+
                    })
+
                },
+
            ))
+
        }
+

+
        Ok::<_, Error>(actions)
+
    }
+

+
    fn publish_draft(
+
        &self,
+
        rid: identity::RepoId,
+
        cob_id: git::Oid,
+
        type_name: cob::TypeName,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+
        let draft_oid = repo.backend.refname_to_id(&draft::cob(
+
            signer.public_key(),
+
            &type_name,
+
            &cob_id.into(),
+
        ))?;
+
        repo.update(
+
            signer.public_key(),
+
            &type_name,
+
            &cob_id.into(),
+
            &draft_oid.into(),
+
        )?;
+

+
        let mut patches = profile.patches_mut(&repo)?;
+
        patches.write(&cob_id.into())?;
+

+
        storage::git::cob::DraftStore::new(&repo, *signer.public_key()).remove(
+
            signer.public_key(),
+
            &type_name,
+
            &cob_id.into(),
+
        )?;
+

+
        Ok::<_, Error>(())
+
    }
+
}
added crates/radicle-types/src/traits/issue.rs
@@ -0,0 +1,150 @@
+
use radicle::issue::cache::Issues as _;
+
use radicle::node::Handle;
+
use radicle::storage::ReadStorage;
+
use radicle::{git, identity, Node};
+

+
use crate::cobs;
+
use crate::error::Error;
+
use crate::traits::Profile;
+

+
pub trait Issues: Profile {
+
    fn list_issues(
+
        &self,
+
        rid: identity::RepoId,
+
        status: Option<cobs::query::IssueStatus>,
+
    ) -> Result<Vec<cobs::issue::Issue>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let status = status.unwrap_or_default();
+
        let issues = profile.issues(&repo)?;
+
        let mut issues: Vec<_> = issues
+
            .list()?
+
            .filter_map(|r| {
+
                let (id, issue) = r.ok()?;
+
                (status.matches(issue.state())).then_some((id, issue))
+
            })
+
            .collect::<Vec<_>>();
+

+
        issues.sort_by(|(_, a), (_, b)| b.timestamp().cmp(&a.timestamp()));
+
        let aliases = &profile.aliases();
+
        let issues = issues
+
            .into_iter()
+
            .map(|(id, issue)| cobs::issue::Issue::new(&id, &issue, aliases))
+
            .collect::<Vec<_>>();
+

+
        Ok::<_, Error>(issues)
+
    }
+

+
    fn issue_by_id(
+
        &self,
+
        rid: identity::RepoId,
+
        id: git::Oid,
+
    ) -> Result<Option<cobs::issue::Issue>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let issues = profile.issues(&repo)?;
+
        let issue = issues.get(&id.into())?;
+

+
        let aliases = &profile.aliases();
+
        let issue = issue.map(|issue| cobs::issue::Issue::new(&id.into(), &issue, aliases));
+

+
        Ok::<_, Error>(issue)
+
    }
+
}
+

+
pub trait IssuesMut: Profile {
+
    fn create_issue(
+
        &self,
+
        rid: identity::RepoId,
+
        new: cobs::issue::NewIssue,
+
        opts: cobs::CobOptions,
+
    ) -> Result<cobs::issue::Issue, Error> {
+
        let profile = self.profile();
+
        let mut node = Node::new(profile.socket());
+
        let repo = profile.storage.repository(rid)?;
+
        let signer = profile.signer()?;
+
        let aliases = profile.aliases();
+
        let mut issues = profile.issues_mut(&repo)?;
+
        let issue = issues.create(
+
            new.title,
+
            new.description,
+
            &new.labels,
+
            &new.assignees,
+
            new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
            &signer,
+
        )?;
+

+
        if opts.announce() {
+
            node.announce_refs(rid)?;
+
        }
+

+
        Ok::<_, Error>(cobs::issue::Issue::new(issue.id(), &issue, &aliases))
+
    }
+

+
    fn edit_issue(
+
        &self,
+
        rid: identity::RepoId,
+
        cob_id: git::Oid,
+
        action: cobs::issue::Action,
+
        opts: cobs::CobOptions,
+
    ) -> Result<cobs::issue::Issue, Error> {
+
        let profile = self.profile();
+
        let mut node = Node::new(profile.socket());
+
        let repo = profile.storage.repository(rid)?;
+
        let signer = profile.signer()?;
+
        let aliases = profile.aliases();
+
        let mut issues = profile.issues_mut(&repo)?;
+
        let mut issue = issues.get_mut(&cob_id.into())?;
+

+
        match action {
+
            cobs::issue::Action::Lifecycle { state } => {
+
                issue.lifecycle(state.into(), &signer)?;
+
            }
+
            cobs::issue::Action::Assign { assignees } => {
+
                issue.assign(assignees, &signer)?;
+
            }
+
            cobs::issue::Action::Label { labels } => {
+
                issue.label(labels, &signer)?;
+
            }
+
            cobs::issue::Action::CommentReact {
+
                id,
+
                reaction,
+
                active,
+
            } => {
+
                issue.react(id, reaction, active, &signer)?;
+
            }
+
            cobs::issue::Action::CommentRedact { id } => {
+
                issue.redact_comment(id, &signer)?;
+
            }
+
            cobs::issue::Action::Comment {
+
                body,
+
                reply_to,
+
                embeds,
+
            } => {
+
                issue.comment(
+
                    body,
+
                    reply_to.unwrap_or(cob_id),
+
                    embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                    &signer,
+
                )?;
+
            }
+
            cobs::issue::Action::CommentEdit { id, body, embeds } => {
+
                issue.edit_comment(
+
                    id,
+
                    body,
+
                    embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                    &signer,
+
                )?;
+
            }
+
            cobs::issue::Action::Edit { title } => {
+
                issue.edit(title, &signer)?;
+
            }
+
        }
+

+
        if opts.announce() {
+
            node.announce_refs(rid)?;
+
        }
+

+
        Ok::<_, Error>(cobs::issue::Issue::new(issue.id(), &issue, &aliases))
+
    }
+
}
added crates/radicle-types/src/traits/patch.rs
@@ -0,0 +1,421 @@
+
use radicle::node::Handle;
+
use radicle::patch::cache::Patches as _;
+
use radicle::storage;
+
use radicle::storage::ReadStorage;
+
use radicle::{cob, git, identity, patch, Node};
+

+
use crate::cobs;
+
use crate::error::Error;
+
use crate::traits::Profile;
+

+
pub trait Patches: Profile {
+
    fn list_patches(
+
        &self,
+
        rid: identity::RepoId,
+
        status: Option<cobs::query::PatchStatus>,
+
        skip: Option<usize>,
+
        take: Option<usize>,
+
    ) -> Result<cobs::PaginatedQuery<Vec<cobs::patch::Patch>>, Error> {
+
        let profile = self.profile();
+
        let cursor = skip.unwrap_or(0);
+
        let take = take.unwrap_or(20);
+
        let repo = profile.storage.repository(rid)?;
+
        let aliases = &profile.aliases();
+
        let cache = profile.patches(&repo)?;
+
        let patches = match status {
+
            None => cache.list()?.collect::<Vec<_>>(),
+
            Some(s) => cache.list_by_status(&s.into())?.collect::<Vec<_>>(),
+
        };
+
        let more = cursor + take < patches.len();
+

+
        let mut patches = patches
+
            .into_iter()
+
            .filter_map(|p| {
+
                p.map(|(id, patch)| cobs::patch::Patch::new(id, &patch, aliases))
+
                    .ok()
+
            })
+
            .skip(cursor)
+
            .take(take)
+
            .collect::<Vec<_>>();
+

+
        patches.sort_by_key(|b| std::cmp::Reverse(b.timestamp()));
+

+
        Ok::<_, Error>(cobs::PaginatedQuery {
+
            cursor,
+
            more,
+
            content: patches,
+
        })
+
    }
+

+
    fn get_patch(
+
        &self,
+
        rid: identity::RepoId,
+
        id: git::Oid,
+
    ) -> Result<Option<cobs::patch::Patch>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let patches = profile.patches(&repo)?;
+
        let patch = patches.get(&id.into())?;
+
        let aliases = &profile.aliases();
+
        let patches = patch.map(|patch| cobs::patch::Patch::new(id.into(), &patch, aliases));
+

+
        Ok::<_, Error>(patches)
+
    }
+

+
    fn revisions_by_patch(
+
        &self,
+
        rid: identity::RepoId,
+
        id: git::Oid,
+
    ) -> Result<Option<Vec<cobs::patch::Revision>>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let patches = profile.patches(&repo)?;
+
        let revisions = patches.get(&id.into())?.map(|patch| {
+
            let aliases = &profile.aliases();
+

+
            patch
+
                .revisions()
+
                .map(|(_, r)| cobs::patch::Revision::new(r.clone(), aliases))
+
                .collect::<Vec<_>>()
+
        });
+

+
        Ok::<_, Error>(revisions)
+
    }
+

+
    fn revision_by_id(
+
        &self,
+
        rid: identity::RepoId,
+
        id: git::Oid,
+
        revision_id: git::Oid,
+
    ) -> Result<Option<cobs::patch::Revision>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let patches = profile.patches(&repo)?;
+
        let revision = patches.get(&id.into())?.and_then(|patch| {
+
            let aliases = &profile.aliases();
+

+
            patch
+
                .revision(&revision_id.into())
+
                .map(|r| cobs::patch::Revision::new(r.clone(), aliases))
+
        });
+

+
        Ok::<_, Error>(revision)
+
    }
+
}
+

+
pub trait PatchesMut: Profile {
+
    fn edit_patch(
+
        &self,
+
        rid: identity::RepoId,
+
        cob_id: git::Oid,
+
        action: cobs::patch::Action,
+
        opts: cobs::CobOptions,
+
    ) -> Result<cobs::patch::Patch, Error> {
+
        let profile = self.profile();
+
        let mut node = Node::new(profile.socket());
+
        let repo = profile.storage.repository(rid)?;
+
        let signer = profile.signer()?;
+
        let aliases = profile.aliases();
+
        let mut patches = profile.patches_mut(&repo)?;
+
        let mut patch = patches.get_mut(&cob_id.into())?;
+

+
        match action {
+
            cobs::patch::Action::RevisionEdit {
+
                revision,
+
                description,
+
                embeds,
+
            } => {
+
                patch.edit_revision(
+
                    revision,
+
                    description,
+
                    embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                    &signer,
+
                )?;
+
            }
+
            cobs::patch::Action::RevisionCommentRedact { revision, comment } => {
+
                patch.comment_redact(revision, comment, &signer)?;
+
            }
+
            cobs::patch::Action::ReviewCommentRedact { review, comment } => {
+
                patch.redact_review_comment(review, comment, &signer)?;
+
            }
+
            cobs::patch::Action::ReviewCommentReact {
+
                review,
+
                comment,
+
                reaction,
+
                active,
+
            } => {
+
                patch.react_review_comment(review, comment, reaction, active, &signer)?;
+
            }
+
            cobs::patch::Action::ReviewCommentResolve { review, comment } => {
+
                patch.resolve_review_comment(review, comment, &signer)?;
+
            }
+
            cobs::patch::Action::ReviewCommentUnresolve { review, comment } => {
+
                patch.unresolve_review_comment(review, comment, &signer)?;
+
            }
+
            cobs::patch::Action::Edit { title, target } => {
+
                patch.edit(title, target, &signer)?;
+
            }
+
            cobs::patch::Action::ReviewEdit {
+
                review,
+
                summary,
+
                verdict,
+
                labels,
+
            } => {
+
                patch.review_edit(review, verdict, summary, labels, &signer)?;
+
            }
+
            cobs::patch::Action::Review {
+
                revision,
+
                summary,
+
                verdict,
+
                labels,
+
            } => {
+
                patch.review(revision, verdict, summary, labels, &signer)?;
+
            }
+
            cobs::patch::Action::ReviewRedact { review } => {
+
                patch.redact_review(review, &signer)?;
+
            }
+
            cobs::patch::Action::ReviewComment {
+
                review,
+
                body,
+
                location,
+
                reply_to,
+
                embeds,
+
            } => {
+
                patch.review_comment(
+
                    review,
+
                    body,
+
                    location.map(|l| l.into()),
+
                    reply_to,
+
                    embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                    &signer,
+
                )?;
+
            }
+
            cobs::patch::Action::ReviewCommentEdit {
+
                review,
+
                comment,
+
                body,
+
                embeds,
+
            } => {
+
                patch.edit_review_comment(
+
                    review,
+
                    comment,
+
                    body,
+
                    embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                    &signer,
+
                )?;
+
            }
+
            cobs::patch::Action::Lifecycle { state } => {
+
                patch.lifecycle(state, &signer)?;
+
            }
+
            cobs::patch::Action::Assign { assignees } => {
+
                patch.assign(assignees, &signer)?;
+
            }
+
            cobs::patch::Action::Label { labels } => {
+
                patch.label(labels, &signer)?;
+
            }
+
            cobs::patch::Action::RevisionReact {
+
                revision,
+
                reaction,
+
                location,
+
                active,
+
            } => {
+
                patch.react(
+
                    revision,
+
                    reaction,
+
                    location.map(|l| l.into()),
+
                    active,
+
                    &signer,
+
                )?;
+
            }
+
            cobs::patch::Action::RevisionComment {
+
                revision,
+
                location,
+
                body,
+
                reply_to,
+
                embeds,
+
            } => {
+
                patch.comment(
+
                    revision,
+
                    body,
+
                    reply_to,
+
                    location.map(|l| l.into()),
+
                    embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                    &signer,
+
                )?;
+
            }
+
            cobs::patch::Action::RevisionCommentEdit {
+
                revision,
+
                comment,
+
                body,
+
                embeds,
+
            } => {
+
                patch.comment_edit(
+
                    revision,
+
                    comment,
+
                    body,
+
                    embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                    &signer,
+
                )?;
+
            }
+
            cobs::patch::Action::RevisionCommentReact {
+
                revision,
+
                comment,
+
                reaction,
+
                active,
+
            } => {
+
                patch.comment_react(revision, comment, reaction, active, &signer)?;
+
            }
+
            cobs::patch::Action::RevisionRedact { revision } => {
+
                patch.redact(revision, &signer)?;
+
            }
+
            cobs::patch::Action::Merge { .. } => {
+
                unimplemented!("We don't support merging of patches through the desktop")
+
            }
+
            cobs::patch::Action::Revision { .. } => {
+
                unimplemented!("We don't support creating new revisions through the desktop")
+
            }
+
        }
+

+
        if opts.announce() {
+
            node.announce_refs(rid)?;
+
        }
+

+
        Ok::<_, Error>(cobs::patch::Patch::new(*patch.id(), &patch, &aliases))
+
    }
+

+
    /// Gets the draft review of the local user for a specific patch revision in a repository.
+
    ///
+
    /// This Tauri command is used to retrieve a patch review draft for the local user
+
    /// on a given patch revision from a repository.
+
    /// It looks up the repository using the provided repository ID (`rid`) and patch object ID (`cob_id`),
+
    /// and gets the patch review of the local user associated with a specific revision (`revision_id`), if it exists.
+
    fn get_draft_review(
+
        &self,
+
        rid: identity::RepoId,
+
        cob_id: git::Oid,
+
        revision_id: patch::RevisionId,
+
    ) -> Option<patch::Review> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid).ok()?;
+
        let signer = profile.signer().ok()?;
+
        let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
+
        let patches = cob::patch::Cache::no_cache(&drafts).ok()?;
+

+
        let patch = patches.get(&cob_id.into()).ok()?;
+
        let revision = patch.and_then(|p| p.revision(&revision_id).cloned());
+
        let review = revision.and_then(|rev| rev.review_by(signer.public_key()).cloned());
+

+
        review
+
    }
+

+
    /// Edits a draft review for a specific patch revision in a repository.
+
    ///
+
    /// This Tauri command allows users to edit a draft review for a specific patch review.
+
    /// The draft is associated with the user (signer) and the provided patch revision within the repository.
+
    fn edit_draft_review(
+
        &self,
+
        rid: identity::RepoId,
+
        cob_id: git::Oid,
+
        edit: cobs::patch::ReviewEdit,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let signer = profile.signer()?;
+
        let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
+

+
        let mut patches = cob::patch::Cache::no_cache(&drafts)?;
+
        let mut patch = patches.get_mut(&cob_id.into())?;
+
        patch.review_edit(
+
            edit.review_id,
+
            edit.verdict,
+
            edit.summary,
+
            edit.labels,
+
            &signer,
+
        )?;
+

+
        patches.write(&cob_id.into())?;
+

+
        Ok::<_, Error>(())
+
    }
+

+
    /// Creates a draft review for a specific patch revision.
+
    ///
+
    /// This Tauri command allows users to create a new draft review for a specific patch revision.
+
    /// The draft is associated with the user (signer) and the provided patch revision within the repository.
+
    fn create_draft_review(
+
        &self,
+
        rid: identity::RepoId,
+
        revision_id: cob::patch::RevisionId,
+
        cob_id: git::Oid,
+
        labels: Vec<cob::Label>,
+
    ) -> Result<patch::ReviewId, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let signer = profile.signer()?;
+
        let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
+

+
        let mut patches = cob::patch::Cache::no_cache(&drafts)?;
+
        let mut patch = patches.get_mut(&cob_id.into())?;
+
        let revision = patch
+
            .revision(&revision_id)
+
            .ok_or_else(|| Error::WithHint {
+
                err: anyhow::anyhow!("patch revision not found"),
+
                hint: "Not able to find the specified patch revision.",
+
            })?;
+

+
        revision
+
            .review_by(signer.public_key())
+
            .ok_or(Error::WithHint {
+
                err: anyhow::anyhow!("duplicate patch review found"),
+
                hint: "Found an existing draft patch review on this patch revision and repo.",
+
            })?;
+

+
        let review_id = patch.review(
+
            revision.id(),
+
            Some(cob::patch::Verdict::Reject),
+
            None,
+
            labels,
+
            &signer,
+
        )?;
+

+
        patches.write(&cob_id.into())?;
+

+
        Ok::<_, Error>(review_id)
+
    }
+

+
    /// Creates a new review comment on a draft review for a specific patch.
+
    ///
+
    /// This Tauri command is used to add a comment to an existing draft review in a repository.
+
    /// It allows users to comment on a specific location in the code or leave general feedback
+
    /// on a review that belongs to a specific patch.
+
    fn create_draft_review_comment(
+
        &self,
+
        rid: identity::RepoId,
+
        cob_id: git::Oid,
+
        new: cobs::thread::CreateReviewComment,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let signer = profile.signer()?;
+
        let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
+

+
        let mut patches = cob::patch::Cache::no_cache(&drafts)?;
+
        let mut patch = patches.get_mut(&cob_id.into())?;
+

+
        patch.transaction("Review comments", &signer, |tx| {
+
            tx.review_comment(
+
                new.review_id,
+
                new.body,
+
                new.location.map(|l| l.into()),
+
                new.reply_to,
+
                new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
            )?;
+

+
            Ok(())
+
        })?;
+

+
        patches.write(&cob_id.into())?;
+

+
        Ok::<_, Error>(())
+
    }
+
}
added crates/radicle-types/src/traits/repo.rs
@@ -0,0 +1,142 @@
+
use radicle::crypto::Verified;
+
use radicle::identity::doc::PayloadId;
+
use radicle::identity::DocAt;
+
use radicle::issue::cache::Issues as _;
+
use radicle::node::routing::Store;
+
use radicle::patch::cache::Patches as _;
+
use radicle::prelude::Doc;
+
use radicle::storage;
+
use radicle::storage::ReadRepository;
+
use radicle::storage::ReadStorage;
+
use radicle::{git, identity};
+
use radicle_surf as surf;
+

+
use crate::cobs;
+
use crate::cobs::diff::Options;
+
use crate::error::Error;
+
use crate::repo;
+
use crate::traits::Profile;
+

+
pub trait Repo: Profile {
+
    fn list_repos(&self) -> Result<Vec<repo::RepoInfo>, Error> {
+
        let profile = self.profile();
+
        let storage = &profile.storage;
+
        let policies = profile.policies()?;
+
        let mut repos = storage.repositories()?.into_iter().collect::<Vec<_>>();
+
        repos.sort_by_key(|p| p.rid);
+

+
        let infos = repos
+
            .into_iter()
+
            .filter_map(|info| {
+
                if !policies.is_seeding(&info.rid).unwrap_or_default() {
+
                    return None;
+
                }
+
                let repo = profile.storage.repository(info.rid).ok()?;
+
                let DocAt { doc, .. } = repo.identity_doc().ok()?;
+
                let repo_info = self.repo_info(&repo, &doc).ok()?;
+

+
                Some(repo_info)
+
            })
+
            .collect::<Vec<_>>();
+

+
        Ok::<_, Error>(infos)
+
    }
+

+
    fn repo_by_id(&self, rid: identity::RepoId) -> Result<repo::RepoInfo, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let DocAt { doc, .. } = repo.identity_doc()?;
+

+
        let repo_info = self.repo_info(&repo, &doc)?;
+

+
        Ok::<_, Error>(repo_info)
+
    }
+

+
    fn diff_stats(
+
        &self,
+
        rid: identity::RepoId,
+
        base: git::Oid,
+
        head: git::Oid,
+
    ) -> Result<cobs::Stats, Error> {
+
        let profile = self.profile();
+
        let repo = radicle_surf::Repository::open(storage::git::paths::repository(
+
            &profile.storage,
+
            &rid,
+
        ))?;
+
        let base = repo.commit(base)?;
+
        let commit = repo.commit(head)?;
+
        let diff = repo.diff(base.id, commit.id)?;
+
        let stats = diff.stats();
+

+
        Ok::<_, Error>(cobs::Stats::new(stats))
+
    }
+

+
    fn repo_info(
+
        &self,
+
        repo: &storage::git::Repository,
+
        doc: &Doc<Verified>,
+
    ) -> Result<repo::RepoInfo, Error> {
+
        let profile = self.profile();
+
        let aliases = profile.aliases();
+
        let delegates = doc
+
            .delegates
+
            .clone()
+
            .into_iter()
+
            .map(|did| cobs::Author::new(did, &aliases))
+
            .collect::<Vec<_>>();
+
        let db = profile.database()?;
+
        let seeding = db.count(&repo.id).unwrap_or_default();
+
        let project = doc.payload.get(&PayloadId::project()).and_then(|payload| {
+
            let (_, head) = repo.head().ok()?;
+
            let commit = repo.commit(head).ok()?;
+
            let patches = profile.patches(repo).ok()?;
+
            let patches = patches.counts().ok()?;
+
            let issues = profile.issues(repo).ok()?;
+
            let issues = issues.counts().ok()?;
+

+
            let data: repo::ProjectPayloadData = (*payload).clone().try_into().ok()?;
+
            let meta = repo::ProjectPayloadMeta {
+
                issues,
+
                patches,
+
                head,
+
                last_commit_timestamp: commit.time().seconds() * 1000,
+
            };
+

+
            Some(repo::ProjectPayload::new(data, meta))
+
        });
+

+
        Ok::<_, Error>(repo::RepoInfo {
+
            payloads: repo::SupportedPayloads { project },
+
            delegates,
+
            threshold: doc.threshold,
+
            visibility: doc.visibility.clone().into(),
+
            rid: repo.id,
+
            seeding,
+
        })
+
    }
+

+
    fn get_diff(&self, rid: identity::RepoId, options: Options) -> Result<surf::diff::Diff, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?.backend;
+
        let base = repo.find_commit(*options.base)?;
+
        let head = repo.find_commit(*options.head)?;
+

+
        let mut opts = git::raw::DiffOptions::new();
+
        opts.patience(true)
+
            .minimal(true)
+
            .context_lines(options.unified);
+

+
        let mut find_opts = git::raw::DiffFindOptions::new();
+
        find_opts.exact_match_only(true);
+
        find_opts.all(true);
+

+
        let left = base.tree()?;
+
        let right = head.tree()?;
+

+
        let mut diff = repo.diff_tree_to_tree(Some(&left), Some(&right), Some(&mut opts))?;
+
        diff.find_similar(Some(&mut find_opts))?;
+
        let diff = surf::diff::Diff::try_from(diff)?;
+

+
        Ok::<_, Error>(diff)
+
    }
+
}
added crates/radicle-types/src/traits/thread.rs
@@ -0,0 +1,121 @@
+
use base64::{engine::general_purpose::STANDARD, Engine as _};
+
use localtime::LocalTime;
+

+
use radicle::cob;
+
use radicle::git;
+
use radicle::identity;
+
use radicle::node::Handle;
+
use radicle::storage::ReadRepository;
+
use radicle::storage::ReadStorage;
+
use radicle::Node;
+

+
use crate::cobs;
+
use crate::error::Error;
+
use crate::traits::Profile;
+

+
pub trait Thread: Profile {
+
    fn get_embed(&self, rid: identity::RepoId, oid: git::Oid) -> Result<String, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let blob = repo.blob(oid)?;
+

+
        Ok::<_, Error>(STANDARD.encode(blob.content()))
+
    }
+

+
    fn save_embed(
+
        &self,
+
        rid: identity::RepoId,
+
        name: &str,
+
        bytes: &[u8],
+
    ) -> Result<git::Oid, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let embed = radicle::cob::Embed::<git::Oid>::store(name, bytes, &repo.backend)?;
+

+
        Ok(embed.oid())
+
    }
+

+
    fn create_issue_comment(
+
        &self,
+
        rid: identity::RepoId,
+
        new: cobs::thread::NewIssueComment,
+
        opts: cobs::CobOptions,
+
    ) -> Result<cobs::thread::Comment<cobs::Never>, Error> {
+
        let profile = self.profile();
+
        let aliases = &profile.aliases();
+
        let mut node = Node::new(profile.socket());
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+
        let mut issues = profile.issues_mut(&repo)?;
+
        let mut issue = issues.get_mut(&new.id.into())?;
+
        let id = new.reply_to.unwrap_or_else(|| {
+
            let (root_id, _) = issue.root();
+
            *root_id
+
        });
+
        let n = new.clone();
+
        let oid = issue.comment(
+
            n.body,
+
            id,
+
            n.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
            &signer,
+
        )?;
+

+
        if opts.announce() {
+
            node.announce_refs(rid)?;
+
        }
+

+
        Ok(cobs::thread::Comment::<cobs::Never>::new(
+
            oid,
+
            cob::thread::Comment::new(
+
                *signer.public_key(),
+
                new.body,
+
                id.into(),
+
                None,
+
                new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                LocalTime::now().into(),
+
            ),
+
            aliases,
+
        ))
+
    }
+

+
    fn create_patch_comment(
+
        &self,
+
        rid: identity::RepoId,
+
        new: cobs::thread::NewPatchComment,
+
        opts: cobs::CobOptions,
+
    ) -> Result<cobs::thread::Comment<cobs::thread::CodeLocation>, Error> {
+
        let profile = self.profile();
+
        let aliases = &profile.aliases();
+
        let mut node = Node::new(profile.socket());
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+
        let mut patches = profile.patches_mut(&repo)?;
+
        let mut patch = patches.get_mut(&new.id.into())?;
+
        let n = new.clone();
+
        let oid = patch.comment(
+
            new.revision.into(),
+
            n.body,
+
            n.reply_to,
+
            n.location.map(|l| l.into()),
+
            n.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
            &signer,
+
        )?;
+

+
        if opts.announce() {
+
            node.announce_refs(rid)?;
+
        }
+

+
        Ok(cobs::thread::Comment::<cobs::thread::CodeLocation>::new(
+
            oid,
+
            cob::thread::Comment::new(
+
                *signer.public_key(),
+
                new.body,
+
                new.reply_to,
+
                new.location.map(|l| l.into()),
+
                new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                LocalTime::now().into(),
+
            ),
+
            aliases,
+
        ))
+
    }
+
}
added crates/test-http-api/Cargo.lock
@@ -0,0 +1,2810 @@
+
# This file is automatically @generated by Cargo.
+
# It is not intended for manual editing.
+
version = 3
+

+
[[package]]
+
name = "addr2line"
+
version = "0.22.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
+
dependencies = [
+
 "gimli",
+
]
+

+
[[package]]
+
name = "adler"
+
version = "1.0.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+

+
[[package]]
+
name = "adler2"
+
version = "2.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+

+
[[package]]
+
name = "aead"
+
version = "0.5.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+
dependencies = [
+
 "crypto-common",
+
 "generic-array",
+
]
+

+
[[package]]
+
name = "aes"
+
version = "0.8.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+
dependencies = [
+
 "cfg-if",
+
 "cipher",
+
 "cpufeatures",
+
]
+

+
[[package]]
+
name = "aes-gcm"
+
version = "0.10.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
+
dependencies = [
+
 "aead",
+
 "aes",
+
 "cipher",
+
 "ctr",
+
 "ghash",
+
 "subtle",
+
]
+

+
[[package]]
+
name = "ahash"
+
version = "0.8.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+
dependencies = [
+
 "cfg-if",
+
 "once_cell",
+
 "version_check",
+
 "zerocopy",
+
]
+

+
[[package]]
+
name = "aho-corasick"
+
version = "1.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+
dependencies = [
+
 "memchr",
+
]
+

+
[[package]]
+
name = "allocator-api2"
+
version = "0.2.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+

+
[[package]]
+
name = "amplify"
+
version = "4.7.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7147b742325842988dd6c793d55f58df3ae36bccf7d9b6e07db10ab035be343d"
+
dependencies = [
+
 "amplify_derive",
+
 "amplify_num",
+
 "ascii",
+
 "wasm-bindgen",
+
]
+

+
[[package]]
+
name = "amplify_derive"
+
version = "4.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2a6309e6b8d89b36b9f959b7a8fa093583b94922a0f6438a24fb08936de4d428"
+
dependencies = [
+
 "amplify_syn",
+
 "proc-macro2",
+
 "quote",
+
 "syn 1.0.109",
+
]
+

+
[[package]]
+
name = "amplify_num"
+
version = "0.5.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "99bcb75a2982047f733547042fc3968c0f460dfcf7d90b90dea3b2744580e9ad"
+
dependencies = [
+
 "wasm-bindgen",
+
]
+

+
[[package]]
+
name = "amplify_syn"
+
version = "2.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7736fb8d473c0d83098b5bac44df6a561e20470375cd8bcae30516dc889fd62a"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 1.0.109",
+
]
+

+
[[package]]
+
name = "android-tzdata"
+
version = "0.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+

+
[[package]]
+
name = "android_system_properties"
+
version = "0.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
+
name = "anstyle-query"
+
version = "1.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
+
dependencies = [
+
 "windows-sys 0.52.0",
+
]
+

+
[[package]]
+
name = "anyhow"
+
version = "1.0.86"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
+

+
[[package]]
+
name = "ascii"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
+

+
[[package]]
+
name = "async-trait"
+
version = "0.1.82"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.77",
+
]
+

+
[[package]]
+
name = "autocfg"
+
version = "1.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+

+
[[package]]
+
name = "axum"
+
version = "0.7.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
+
dependencies = [
+
 "async-trait",
+
 "axum-core",
+
 "bytes",
+
 "futures-util",
+
 "http",
+
 "http-body",
+
 "http-body-util",
+
 "hyper",
+
 "hyper-util",
+
 "itoa",
+
 "matchit",
+
 "memchr",
+
 "mime",
+
 "percent-encoding",
+
 "pin-project-lite",
+
 "rustversion",
+
 "serde",
+
 "serde_json",
+
 "serde_path_to_error",
+
 "serde_urlencoded",
+
 "sync_wrapper 1.0.1",
+
 "tokio",
+
 "tower 0.4.13",
+
 "tower-layer",
+
 "tower-service",
+
]
+

+
[[package]]
+
name = "axum-core"
+
version = "0.4.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3"
+
dependencies = [
+
 "async-trait",
+
 "bytes",
+
 "futures-util",
+
 "http",
+
 "http-body",
+
 "http-body-util",
+
 "mime",
+
 "pin-project-lite",
+
 "rustversion",
+
 "sync_wrapper 0.1.2",
+
 "tower-layer",
+
 "tower-service",
+
]
+

+
[[package]]
+
name = "backtrace"
+
version = "0.3.73"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
+
dependencies = [
+
 "addr2line",
+
 "cc",
+
 "cfg-if",
+
 "libc",
+
 "miniz_oxide 0.7.4",
+
 "object",
+
 "rustc-demangle",
+
]
+

+
[[package]]
+
name = "base-x"
+
version = "0.2.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
+

+
[[package]]
+
name = "base16ct"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+

+
[[package]]
+
name = "base32"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
+

+
[[package]]
+
name = "base64"
+
version = "0.21.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+

+
[[package]]
+
name = "base64"
+
version = "0.22.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+

+
[[package]]
+
name = "base64ct"
+
version = "1.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+

+
[[package]]
+
name = "bcrypt-pbkdf"
+
version = "0.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2"
+
dependencies = [
+
 "blowfish",
+
 "pbkdf2",
+
 "sha2",
+
]
+

+
[[package]]
+
name = "bitflags"
+
version = "1.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+

+
[[package]]
+
name = "bitflags"
+
version = "2.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+

+
[[package]]
+
name = "block-buffer"
+
version = "0.10.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+
dependencies = [
+
 "generic-array",
+
]
+

+
[[package]]
+
name = "block-padding"
+
version = "0.3.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+
dependencies = [
+
 "generic-array",
+
]
+

+
[[package]]
+
name = "blowfish"
+
version = "0.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
+
dependencies = [
+
 "byteorder",
+
 "cipher",
+
]
+

+
[[package]]
+
name = "bumpalo"
+
version = "3.16.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+

+
[[package]]
+
name = "byteorder"
+
version = "1.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+

+
[[package]]
+
name = "bytes"
+
version = "1.7.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
+

+
[[package]]
+
name = "cbc"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+
dependencies = [
+
 "cipher",
+
]
+

+
[[package]]
+
name = "cc"
+
version = "1.1.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
+
dependencies = [
+
 "jobserver",
+
 "libc",
+
 "shlex",
+
]
+

+
[[package]]
+
name = "cfg-if"
+
version = "1.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+

+
[[package]]
+
name = "chacha20"
+
version = "0.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
+
dependencies = [
+
 "cfg-if",
+
 "cipher",
+
 "cpufeatures",
+
]
+

+
[[package]]
+
name = "chrono"
+
version = "0.4.38"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+
dependencies = [
+
 "android-tzdata",
+
 "iana-time-zone",
+
 "num-traits",
+
 "windows-targets",
+
]
+

+
[[package]]
+
name = "cipher"
+
version = "0.4.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+
dependencies = [
+
 "crypto-common",
+
 "inout",
+
]
+

+
[[package]]
+
name = "const-oid"
+
version = "0.9.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+

+
[[package]]
+
name = "core-foundation-sys"
+
version = "0.8.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+

+
[[package]]
+
name = "cpufeatures"
+
version = "0.2.13"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
+
name = "crc32fast"
+
version = "1.4.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+
dependencies = [
+
 "cfg-if",
+
]
+

+
[[package]]
+
name = "crossbeam-channel"
+
version = "0.5.13"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
+
dependencies = [
+
 "crossbeam-utils",
+
]
+

+
[[package]]
+
name = "crossbeam-utils"
+
version = "0.8.20"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
+

+
[[package]]
+
name = "crypto-bigint"
+
version = "0.5.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+
dependencies = [
+
 "generic-array",
+
 "rand_core",
+
 "subtle",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "crypto-common"
+
version = "0.1.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+
dependencies = [
+
 "generic-array",
+
 "typenum",
+
]
+

+
[[package]]
+
name = "ct-codecs"
+
version = "1.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "026ac6ceace6298d2c557ef5ed798894962296469ec7842288ea64674201a2d1"
+

+
[[package]]
+
name = "ctr"
+
version = "0.9.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+
dependencies = [
+
 "cipher",
+
]
+

+
[[package]]
+
name = "cypheraddr"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ba5c54d2ad4ab9941383519471b75d12abc1a7b4779265e233168f2703a730d9"
+
dependencies = [
+
 "amplify",
+
 "base32",
+
 "cyphergraphy",
+
 "sha3",
+
]
+

+
[[package]]
+
name = "cyphergraphy"
+
version = "0.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b67c16c8ef5ddcdab57aab83fd8e770540ea3682ccdae09642c63575b0da2184"
+
dependencies = [
+
 "amplify",
+
 "ec25519",
+
]
+

+
[[package]]
+
name = "cyphernet"
+
version = "0.5.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ac949369884a7a1d802cc669821269c707be8cec4d65043382e253733d2e62e1"
+
dependencies = [
+
 "cypheraddr",
+
 "cyphergraphy",
+
 "socks5-client",
+
]
+

+
[[package]]
+
name = "data-encoding"
+
version = "2.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
+

+
[[package]]
+
name = "data-encoding-macro"
+
version = "0.1.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f1559b6cba622276d6d63706db152618eeb15b89b3e4041446b05876e352e639"
+
dependencies = [
+
 "data-encoding",
+
 "data-encoding-macro-internal",
+
]
+

+
[[package]]
+
name = "data-encoding-macro-internal"
+
version = "0.1.13"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "332d754c0af53bc87c108fed664d121ecf59207ec4196041f04d6ab9002ad33f"
+
dependencies = [
+
 "data-encoding",
+
 "syn 1.0.109",
+
]
+

+
[[package]]
+
name = "der"
+
version = "0.7.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
+
dependencies = [
+
 "const-oid",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "deranged"
+
version = "0.3.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+
dependencies = [
+
 "powerfmt",
+
]
+

+
[[package]]
+
name = "diff"
+
version = "0.1.13"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+

+
[[package]]
+
name = "digest"
+
version = "0.10.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+
dependencies = [
+
 "block-buffer",
+
 "const-oid",
+
 "crypto-common",
+
 "subtle",
+
]
+

+
[[package]]
+
name = "dyn-clone"
+
version = "1.0.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
+

+
[[package]]
+
name = "ec25519"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bdfd533a2fc01178c738c99412ae1f7e1ad2cb37c2e14bfd87e9d4618171c825"
+
dependencies = [
+
 "ct-codecs",
+
 "ed25519",
+
 "getrandom",
+
]
+

+
[[package]]
+
name = "ecdsa"
+
version = "0.16.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
+
dependencies = [
+
 "der",
+
 "digest",
+
 "elliptic-curve",
+
 "rfc6979",
+
 "signature 2.2.0",
+
 "spki",
+
]
+

+
[[package]]
+
name = "ed25519"
+
version = "1.5.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
+
dependencies = [
+
 "signature 1.6.4",
+
]
+

+
[[package]]
+
name = "elliptic-curve"
+
version = "0.13.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+
dependencies = [
+
 "base16ct",
+
 "crypto-bigint",
+
 "digest",
+
 "ff",
+
 "generic-array",
+
 "group",
+
 "pkcs8",
+
 "rand_core",
+
 "sec1",
+
 "subtle",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "equivalent"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+

+
[[package]]
+
name = "errno"
+
version = "0.3.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+
dependencies = [
+
 "libc",
+
 "windows-sys 0.52.0",
+
]
+

+
[[package]]
+
name = "fastrand"
+
version = "2.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
+

+
[[package]]
+
name = "ff"
+
version = "0.13.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
+
dependencies = [
+
 "rand_core",
+
 "subtle",
+
]
+

+
[[package]]
+
name = "filetime"
+
version = "0.2.25"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
+
dependencies = [
+
 "cfg-if",
+
 "libc",
+
 "libredox 0.1.3",
+
 "windows-sys 0.59.0",
+
]
+

+
[[package]]
+
name = "flate2"
+
version = "1.0.33"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253"
+
dependencies = [
+
 "crc32fast",
+
 "miniz_oxide 0.8.0",
+
]
+

+
[[package]]
+
name = "fnv"
+
version = "1.0.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+

+
[[package]]
+
name = "form_urlencoded"
+
version = "1.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+
dependencies = [
+
 "percent-encoding",
+
]
+

+
[[package]]
+
name = "futures-channel"
+
version = "0.3.30"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+
dependencies = [
+
 "futures-core",
+
]
+

+
[[package]]
+
name = "futures-core"
+
version = "0.3.30"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+

+
[[package]]
+
name = "futures-task"
+
version = "0.3.30"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+

+
[[package]]
+
name = "futures-util"
+
version = "0.3.30"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+
dependencies = [
+
 "futures-core",
+
 "futures-task",
+
 "pin-project-lite",
+
 "pin-utils",
+
]
+

+
[[package]]
+
name = "fxhash"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+
dependencies = [
+
 "byteorder",
+
]
+

+
[[package]]
+
name = "generic-array"
+
version = "0.14.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+
dependencies = [
+
 "typenum",
+
 "version_check",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "getrandom"
+
version = "0.2.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+
dependencies = [
+
 "cfg-if",
+
 "libc",
+
 "wasi",
+
]
+

+
[[package]]
+
name = "ghash"
+
version = "0.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
+
dependencies = [
+
 "opaque-debug",
+
 "polyval",
+
]
+

+
[[package]]
+
name = "gimli"
+
version = "0.29.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
+

+
[[package]]
+
name = "git-ref-format"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7428e0d6e549a9a613d6f019b839a0f5142c331295b79e119ca8f4faac145da1"
+
dependencies = [
+
 "git-ref-format-core",
+
 "git-ref-format-macro",
+
]
+

+
[[package]]
+
name = "git-ref-format-core"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bbaeb9672a55e9e32cb6d3ef781e7526b25ab97d499fae71615649340b143424"
+
dependencies = [
+
 "serde",
+
 "thiserror",
+
]
+

+
[[package]]
+
name = "git-ref-format-macro"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3b6ca5353accc201f6324dff744ba4660099546d4daf187ba868f07562e36ca4"
+
dependencies = [
+
 "git-ref-format-core",
+
 "proc-macro-error",
+
 "quote",
+
 "syn 2.0.77",
+
]
+

+
[[package]]
+
name = "git2"
+
version = "0.19.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
+
dependencies = [
+
 "bitflags 2.6.0",
+
 "libc",
+
 "libgit2-sys",
+
 "log",
+
 "url",
+
]
+

+
[[package]]
+
name = "group"
+
version = "0.13.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+
dependencies = [
+
 "ff",
+
 "rand_core",
+
 "subtle",
+
]
+

+
[[package]]
+
name = "hashbrown"
+
version = "0.14.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
dependencies = [
+
 "ahash",
+
 "allocator-api2",
+
]
+

+
[[package]]
+
name = "hermit-abi"
+
version = "0.3.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+

+
[[package]]
+
name = "hmac"
+
version = "0.12.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+
dependencies = [
+
 "digest",
+
]
+

+
[[package]]
+
name = "http"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+
dependencies = [
+
 "bytes",
+
 "fnv",
+
 "itoa",
+
]
+

+
[[package]]
+
name = "http-body"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+
dependencies = [
+
 "bytes",
+
 "http",
+
]
+

+
[[package]]
+
name = "http-body-util"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
+
dependencies = [
+
 "bytes",
+
 "futures-util",
+
 "http",
+
 "http-body",
+
 "pin-project-lite",
+
]
+

+
[[package]]
+
name = "httparse"
+
version = "1.9.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
+

+
[[package]]
+
name = "httpdate"
+
version = "1.0.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+

+
[[package]]
+
name = "hyper"
+
version = "1.4.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
+
dependencies = [
+
 "bytes",
+
 "futures-channel",
+
 "futures-util",
+
 "http",
+
 "http-body",
+
 "httparse",
+
 "httpdate",
+
 "itoa",
+
 "pin-project-lite",
+
 "smallvec",
+
 "tokio",
+
 "want",
+
]
+

+
[[package]]
+
name = "hyper-util"
+
version = "0.1.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9"
+
dependencies = [
+
 "bytes",
+
 "futures-util",
+
 "http",
+
 "http-body",
+
 "hyper",
+
 "pin-project-lite",
+
 "tokio",
+
]
+

+
[[package]]
+
name = "iana-time-zone"
+
version = "0.1.60"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
+
dependencies = [
+
 "android_system_properties",
+
 "core-foundation-sys",
+
 "iana-time-zone-haiku",
+
 "js-sys",
+
 "wasm-bindgen",
+
 "windows-core",
+
]
+

+
[[package]]
+
name = "iana-time-zone-haiku"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+
dependencies = [
+
 "cc",
+
]
+

+
[[package]]
+
name = "idna"
+
version = "0.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+
dependencies = [
+
 "unicode-bidi",
+
 "unicode-normalization",
+
]
+

+
[[package]]
+
name = "indexmap"
+
version = "2.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
+
dependencies = [
+
 "equivalent",
+
 "hashbrown",
+
]
+

+
[[package]]
+
name = "inout"
+
version = "0.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+
dependencies = [
+
 "block-padding",
+
 "generic-array",
+
]
+

+
[[package]]
+
name = "inquire"
+
version = "0.7.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
+
dependencies = [
+
 "bitflags 2.6.0",
+
 "dyn-clone",
+
 "fxhash",
+
 "newline-converter",
+
 "once_cell",
+
 "tempfile",
+
 "termion 2.0.3",
+
 "unicode-segmentation",
+
 "unicode-width",
+
]
+

+
[[package]]
+
name = "itoa"
+
version = "1.0.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+

+
[[package]]
+
name = "jobserver"
+
version = "0.1.32"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
+
name = "js-sys"
+
version = "0.3.70"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
+
dependencies = [
+
 "wasm-bindgen",
+
]
+

+
[[package]]
+
name = "keccak"
+
version = "0.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
+
dependencies = [
+
 "cpufeatures",
+
]
+

+
[[package]]
+
name = "lazy_static"
+
version = "1.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
dependencies = [
+
 "spin",
+
]
+

+
[[package]]
+
name = "lexopt"
+
version = "0.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "baff4b617f7df3d896f97fe922b64817f6cd9a756bb81d40f8883f2f66dcb401"
+

+
[[package]]
+
name = "libc"
+
version = "0.2.158"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
+

+
[[package]]
+
name = "libgit2-sys"
+
version = "0.17.0+1.8.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
+
dependencies = [
+
 "cc",
+
 "libc",
+
 "libz-sys",
+
 "pkg-config",
+
]
+

+
[[package]]
+
name = "libm"
+
version = "0.2.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+

+
[[package]]
+
name = "libredox"
+
version = "0.0.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607"
+
dependencies = [
+
 "bitflags 2.6.0",
+
 "libc",
+
 "redox_syscall 0.4.1",
+
]
+

+
[[package]]
+
name = "libredox"
+
version = "0.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+
dependencies = [
+
 "bitflags 2.6.0",
+
 "libc",
+
 "redox_syscall 0.5.3",
+
]
+

+
[[package]]
+
name = "libz-sys"
+
version = "1.1.20"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
+
dependencies = [
+
 "cc",
+
 "libc",
+
 "pkg-config",
+
 "vcpkg",
+
]
+

+
[[package]]
+
name = "linux-raw-sys"
+
version = "0.4.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+

+
[[package]]
+
name = "localtime"
+
version = "1.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "016a009e0bb8ba6e3229fb74bf11a8fe6ef24542cc6ef35ef38863ac13f96d87"
+
dependencies = [
+
 "serde",
+
]
+

+
[[package]]
+
name = "log"
+
version = "0.4.22"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+

+
[[package]]
+
name = "lru"
+
version = "0.12.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904"
+
dependencies = [
+
 "hashbrown",
+
]
+

+
[[package]]
+
name = "matchers"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+
dependencies = [
+
 "regex-automata 0.1.10",
+
]
+

+
[[package]]
+
name = "matchit"
+
version = "0.7.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+

+
[[package]]
+
name = "memchr"
+
version = "2.7.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+

+
[[package]]
+
name = "mime"
+
version = "0.3.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+

+
[[package]]
+
name = "miniz_oxide"
+
version = "0.7.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
+
dependencies = [
+
 "adler",
+
]
+

+
[[package]]
+
name = "miniz_oxide"
+
version = "0.8.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
+
dependencies = [
+
 "adler2",
+
]
+

+
[[package]]
+
name = "mio"
+
version = "1.0.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
+
dependencies = [
+
 "hermit-abi",
+
 "libc",
+
 "wasi",
+
 "windows-sys 0.52.0",
+
]
+

+
[[package]]
+
name = "multibase"
+
version = "0.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404"
+
dependencies = [
+
 "base-x",
+
 "data-encoding",
+
 "data-encoding-macro",
+
]
+

+
[[package]]
+
name = "newline-converter"
+
version = "0.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
+
dependencies = [
+
 "unicode-segmentation",
+
]
+

+
[[package]]
+
name = "nonempty"
+
version = "0.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "995defdca0a589acfdd1bd2e8e3b896b4d4f7675a31fd14c32611440c7f608e6"
+
dependencies = [
+
 "serde",
+
]
+

+
[[package]]
+
name = "nu-ansi-term"
+
version = "0.46.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+
dependencies = [
+
 "overload",
+
 "winapi",
+
]
+

+
[[package]]
+
name = "num-bigint-dig"
+
version = "0.8.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+
dependencies = [
+
 "byteorder",
+
 "lazy_static",
+
 "libm",
+
 "num-integer",
+
 "num-iter",
+
 "num-traits",
+
 "rand",
+
 "smallvec",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "num-conv"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+

+
[[package]]
+
name = "num-integer"
+
version = "0.1.46"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+
dependencies = [
+
 "num-traits",
+
]
+

+
[[package]]
+
name = "num-iter"
+
version = "0.1.45"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+
dependencies = [
+
 "autocfg",
+
 "num-integer",
+
 "num-traits",
+
]
+

+
[[package]]
+
name = "num-traits"
+
version = "0.2.19"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+
dependencies = [
+
 "autocfg",
+
 "libm",
+
]
+

+
[[package]]
+
name = "numtoa"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
+

+
[[package]]
+
name = "object"
+
version = "0.36.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a"
+
dependencies = [
+
 "memchr",
+
]
+

+
[[package]]
+
name = "once_cell"
+
version = "1.19.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+

+
[[package]]
+
name = "opaque-debug"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+

+
[[package]]
+
name = "overload"
+
version = "0.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+

+
[[package]]
+
name = "p256"
+
version = "0.13.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
+
dependencies = [
+
 "ecdsa",
+
 "elliptic-curve",
+
 "primeorder",
+
 "sha2",
+
]
+

+
[[package]]
+
name = "p384"
+
version = "0.13.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
+
dependencies = [
+
 "ecdsa",
+
 "elliptic-curve",
+
 "primeorder",
+
 "sha2",
+
]
+

+
[[package]]
+
name = "p521"
+
version = "0.13.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
+
dependencies = [
+
 "base16ct",
+
 "ecdsa",
+
 "elliptic-curve",
+
 "primeorder",
+
 "rand_core",
+
 "sha2",
+
]
+

+
[[package]]
+
name = "pbkdf2"
+
version = "0.12.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
+
dependencies = [
+
 "digest",
+
]
+

+
[[package]]
+
name = "pem-rfc7468"
+
version = "0.7.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+
dependencies = [
+
 "base64ct",
+
]
+

+
[[package]]
+
name = "percent-encoding"
+
version = "2.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+

+
[[package]]
+
name = "pin-project"
+
version = "1.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+
dependencies = [
+
 "pin-project-internal",
+
]
+

+
[[package]]
+
name = "pin-project-internal"
+
version = "1.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.77",
+
]
+

+
[[package]]
+
name = "pin-project-lite"
+
version = "0.2.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+

+
[[package]]
+
name = "pin-utils"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+

+
[[package]]
+
name = "pkcs1"
+
version = "0.7.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+
dependencies = [
+
 "der",
+
 "pkcs8",
+
 "spki",
+
]
+

+
[[package]]
+
name = "pkcs8"
+
version = "0.10.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+
dependencies = [
+
 "der",
+
 "spki",
+
]
+

+
[[package]]
+
name = "pkg-config"
+
version = "0.3.30"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+

+
[[package]]
+
name = "poly1305"
+
version = "0.8.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+
dependencies = [
+
 "cpufeatures",
+
 "opaque-debug",
+
 "universal-hash",
+
]
+

+
[[package]]
+
name = "polyval"
+
version = "0.6.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
+
dependencies = [
+
 "cfg-if",
+
 "cpufeatures",
+
 "opaque-debug",
+
 "universal-hash",
+
]
+

+
[[package]]
+
name = "powerfmt"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+

+
[[package]]
+
name = "ppv-lite86"
+
version = "0.2.20"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+
dependencies = [
+
 "zerocopy",
+
]
+

+
[[package]]
+
name = "pretty_assertions"
+
version = "1.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
+
dependencies = [
+
 "diff",
+
 "yansi",
+
]
+

+
[[package]]
+
name = "primeorder"
+
version = "0.13.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
+
dependencies = [
+
 "elliptic-curve",
+
]
+

+
[[package]]
+
name = "proc-macro-error"
+
version = "1.0.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+
dependencies = [
+
 "proc-macro-error-attr",
+
 "proc-macro2",
+
 "quote",
+
 "syn 1.0.109",
+
 "version_check",
+
]
+

+
[[package]]
+
name = "proc-macro-error-attr"
+
version = "1.0.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "version_check",
+
]
+

+
[[package]]
+
name = "proc-macro2"
+
version = "1.0.86"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+
dependencies = [
+
 "unicode-ident",
+
]
+

+
[[package]]
+
name = "qcheck"
+
version = "1.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b439bd4242da51d62d18c95e6a6add749346756b0d1a587dfd0cc22fa6b5f3f0"
+
dependencies = [
+
 "rand",
+
]
+

+
[[package]]
+
name = "quote"
+
version = "1.0.37"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+
dependencies = [
+
 "proc-macro2",
+
]
+

+
[[package]]
+
name = "radicle"
+
version = "0.13.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4a818569c11f1bac56f38b002d778ce8ec92e312024b9aebcd68bad5dee6a465"
+
dependencies = [
+
 "amplify",
+
 "base64 0.21.7",
+
 "crossbeam-channel",
+
 "cyphernet",
+
 "fastrand",
+
 "git2",
+
 "libc",
+
 "localtime",
+
 "log",
+
 "multibase",
+
 "nonempty",
+
 "once_cell",
+
 "radicle-cob",
+
 "radicle-crypto",
+
 "radicle-git-ext",
+
 "radicle-ssh",
+
 "serde",
+
 "serde_json",
+
 "siphasher",
+
 "sqlite",
+
 "tempfile",
+
 "thiserror",
+
 "unicode-normalization",
+
]
+

+
[[package]]
+
name = "radicle-cob"
+
version = "0.12.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d4fac94999d8ffb6e88674bee487b080b69bbc9fb1b439ebfa51481ede1a17b3"
+
dependencies = [
+
 "fastrand",
+
 "git2",
+
 "log",
+
 "nonempty",
+
 "once_cell",
+
 "radicle-crypto",
+
 "radicle-dag",
+
 "radicle-git-ext",
+
 "serde",
+
 "serde_json",
+
 "thiserror",
+
]
+

+
[[package]]
+
name = "radicle-crypto"
+
version = "0.11.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d1d6a67969719841ad06049597006368eb4238ca63a02d20207654dfd1d2d6ad"
+
dependencies = [
+
 "amplify",
+
 "cyphernet",
+
 "ec25519",
+
 "fastrand",
+
 "multibase",
+
 "qcheck",
+
 "radicle-git-ext",
+
 "radicle-ssh",
+
 "serde",
+
 "sqlite",
+
 "ssh-key",
+
 "thiserror",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "radicle-dag"
+
version = "0.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c2a678c3049a88ae6a34dd9f52ea9a5f9f066a0af63466b75cf8c48840303067"
+
dependencies = [
+
 "fastrand",
+
]
+

+
[[package]]
+
name = "radicle-git-ext"
+
version = "0.8.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4b78c26e67d1712ad5a0c602ae3b236609461372ac04e200bda359fe4a1c6650"
+
dependencies = [
+
 "git-ref-format",
+
 "git2",
+
 "percent-encoding",
+
 "radicle-std-ext",
+
 "serde",
+
 "thiserror",
+
]
+

+
[[package]]
+
name = "radicle-httpd"
+
version = "0.16.0"
+
dependencies = [
+
 "anyhow",
+
 "axum",
+
 "base64 0.22.1",
+
 "chrono",
+
 "flate2",
+
 "hyper",
+
 "lexopt",
+
 "lru",
+
 "nonempty",
+
 "pretty_assertions",
+
 "radicle",
+
 "radicle-crypto",
+
 "radicle-surf",
+
 "radicle-term",
+
 "serde",
+
 "serde_json",
+
 "tempfile",
+
 "thiserror",
+
 "tokio",
+
 "tower 0.5.0",
+
 "tower-http",
+
 "tracing",
+
 "tracing-logfmt",
+
 "tracing-subscriber",
+
]
+

+
[[package]]
+
name = "radicle-signals"
+
version = "0.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7fba65f6ed964e6e8d34d935f83d40c506f14bf45e60c635042ca4ad4185f149"
+
dependencies = [
+
 "crossbeam-channel",
+
 "libc",
+
]
+

+
[[package]]
+
name = "radicle-ssh"
+
version = "0.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fbee758010fb64482be4b18591fbeb3cbc15b16450d143edf4edb5484c7366c6"
+
dependencies = [
+
 "byteorder",
+
 "log",
+
 "thiserror",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "radicle-std-ext"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "db20136bbc9ae63f3fec8e5a6c369f4902fac2244501b5dfc6d668e43475aaa4"
+

+
[[package]]
+
name = "radicle-surf"
+
version = "0.22.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0bf6aff57520e8e7200bf7826ddc5ccf8d4612dd880497a4f256c3d272eeb805"
+
dependencies = [
+
 "anyhow",
+
 "base64 0.21.7",
+
 "flate2",
+
 "git2",
+
 "log",
+
 "nonempty",
+
 "radicle-git-ext",
+
 "radicle-std-ext",
+
 "serde",
+
 "tar",
+
 "thiserror",
+
 "url",
+
]
+

+
[[package]]
+
name = "radicle-term"
+
version = "0.11.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d2cf3256980e3fddcd135f6e755022df8b385b842cdcbbfce059f47e87caec18"
+
dependencies = [
+
 "anstyle-query",
+
 "anyhow",
+
 "crossbeam-channel",
+
 "inquire",
+
 "libc",
+
 "once_cell",
+
 "radicle-signals",
+
 "shlex",
+
 "termion 3.0.0",
+
 "thiserror",
+
 "unicode-display-width",
+
 "unicode-segmentation",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "rand"
+
version = "0.8.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+
dependencies = [
+
 "rand_chacha",
+
 "rand_core",
+
]
+

+
[[package]]
+
name = "rand_chacha"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+
dependencies = [
+
 "ppv-lite86",
+
 "rand_core",
+
]
+

+
[[package]]
+
name = "rand_core"
+
version = "0.6.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
dependencies = [
+
 "getrandom",
+
]
+

+
[[package]]
+
name = "redox_syscall"
+
version = "0.4.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+
dependencies = [
+
 "bitflags 1.3.2",
+
]
+

+
[[package]]
+
name = "redox_syscall"
+
version = "0.5.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
+
dependencies = [
+
 "bitflags 2.6.0",
+
]
+

+
[[package]]
+
name = "redox_termios"
+
version = "0.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb"
+

+
[[package]]
+
name = "regex"
+
version = "1.10.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
+
dependencies = [
+
 "aho-corasick",
+
 "memchr",
+
 "regex-automata 0.4.7",
+
 "regex-syntax 0.8.4",
+
]
+

+
[[package]]
+
name = "regex-automata"
+
version = "0.1.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+
dependencies = [
+
 "regex-syntax 0.6.29",
+
]
+

+
[[package]]
+
name = "regex-automata"
+
version = "0.4.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+
dependencies = [
+
 "aho-corasick",
+
 "memchr",
+
 "regex-syntax 0.8.4",
+
]
+

+
[[package]]
+
name = "regex-syntax"
+
version = "0.6.29"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+

+
[[package]]
+
name = "regex-syntax"
+
version = "0.8.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+

+
[[package]]
+
name = "rfc6979"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+
dependencies = [
+
 "hmac",
+
 "subtle",
+
]
+

+
[[package]]
+
name = "rsa"
+
version = "0.9.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
+
dependencies = [
+
 "const-oid",
+
 "digest",
+
 "num-bigint-dig",
+
 "num-integer",
+
 "num-traits",
+
 "pkcs1",
+
 "pkcs8",
+
 "rand_core",
+
 "sha2",
+
 "signature 2.2.0",
+
 "spki",
+
 "subtle",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "rustc-demangle"
+
version = "0.1.24"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+

+
[[package]]
+
name = "rustix"
+
version = "0.38.35"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f"
+
dependencies = [
+
 "bitflags 2.6.0",
+
 "errno",
+
 "libc",
+
 "linux-raw-sys",
+
 "windows-sys 0.52.0",
+
]
+

+
[[package]]
+
name = "rustversion"
+
version = "1.0.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
+

+
[[package]]
+
name = "ryu"
+
version = "1.0.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+

+
[[package]]
+
name = "sec1"
+
version = "0.7.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+
dependencies = [
+
 "base16ct",
+
 "der",
+
 "generic-array",
+
 "pkcs8",
+
 "subtle",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "serde"
+
version = "1.0.209"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
+
dependencies = [
+
 "serde_derive",
+
]
+

+
[[package]]
+
name = "serde_derive"
+
version = "1.0.209"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.77",
+
]
+

+
[[package]]
+
name = "serde_json"
+
version = "1.0.127"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
+
dependencies = [
+
 "indexmap",
+
 "itoa",
+
 "memchr",
+
 "ryu",
+
 "serde",
+
]
+

+
[[package]]
+
name = "serde_path_to_error"
+
version = "0.1.16"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
+
dependencies = [
+
 "itoa",
+
 "serde",
+
]
+

+
[[package]]
+
name = "serde_urlencoded"
+
version = "0.7.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+
dependencies = [
+
 "form_urlencoded",
+
 "itoa",
+
 "ryu",
+
 "serde",
+
]
+

+
[[package]]
+
name = "sha2"
+
version = "0.10.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+
dependencies = [
+
 "cfg-if",
+
 "cpufeatures",
+
 "digest",
+
]
+

+
[[package]]
+
name = "sha3"
+
version = "0.10.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
+
dependencies = [
+
 "digest",
+
 "keccak",
+
]
+

+
[[package]]
+
name = "sharded-slab"
+
version = "0.1.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+
dependencies = [
+
 "lazy_static",
+
]
+

+
[[package]]
+
name = "shlex"
+
version = "1.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+

+
[[package]]
+
name = "signature"
+
version = "1.6.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
+

+
[[package]]
+
name = "signature"
+
version = "2.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+
dependencies = [
+
 "digest",
+
 "rand_core",
+
]
+

+
[[package]]
+
name = "siphasher"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
+

+
[[package]]
+
name = "smallvec"
+
version = "1.13.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+

+
[[package]]
+
name = "socket2"
+
version = "0.5.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+
dependencies = [
+
 "libc",
+
 "windows-sys 0.52.0",
+
]
+

+
[[package]]
+
name = "socks5-client"
+
version = "0.4.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ffc7dcf6fab1d65d82d633006a4cc658d76ce436e01cf1a7c71873c0eeba324c"
+
dependencies = [
+
 "amplify",
+
 "cypheraddr",
+
]
+

+
[[package]]
+
name = "spin"
+
version = "0.9.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+

+
[[package]]
+
name = "spki"
+
version = "0.7.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+
dependencies = [
+
 "base64ct",
+
 "der",
+
]
+

+
[[package]]
+
name = "sqlite"
+
version = "0.32.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "03801c10193857d6a4a71ec46cee198a15cbc659622aabe1db0d0bdbefbcf8e6"
+
dependencies = [
+
 "libc",
+
 "sqlite3-sys",
+
]
+

+
[[package]]
+
name = "sqlite3-src"
+
version = "0.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bfc95a51a1ee38839599371685b9d4a926abb51791f0bc3bf8c3bb7867e6e454"
+
dependencies = [
+
 "cc",
+
 "pkg-config",
+
]
+

+
[[package]]
+
name = "sqlite3-sys"
+
version = "0.15.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f2752c669433e40ebb08fde824146f50d9628aa0b66a3b7fc6be34db82a8063b"
+
dependencies = [
+
 "libc",
+
 "sqlite3-src",
+
]
+

+
[[package]]
+
name = "ssh-cipher"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
+
dependencies = [
+
 "aes",
+
 "aes-gcm",
+
 "cbc",
+
 "chacha20",
+
 "cipher",
+
 "ctr",
+
 "poly1305",
+
 "ssh-encoding",
+
 "subtle",
+
]
+

+
[[package]]
+
name = "ssh-encoding"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
+
dependencies = [
+
 "base64ct",
+
 "pem-rfc7468",
+
 "sha2",
+
]
+

+
[[package]]
+
name = "ssh-key"
+
version = "0.6.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ca9b366a80cf18bb6406f4cf4d10aebfb46140a8c0c33f666a144c5c76ecbafc"
+
dependencies = [
+
 "bcrypt-pbkdf",
+
 "p256",
+
 "p384",
+
 "p521",
+
 "rand_core",
+
 "rsa",
+
 "sec1",
+
 "sha2",
+
 "signature 2.2.0",
+
 "ssh-cipher",
+
 "ssh-encoding",
+
 "subtle",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "subtle"
+
version = "2.6.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+

+
[[package]]
+
name = "syn"
+
version = "1.0.109"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "unicode-ident",
+
]
+

+
[[package]]
+
name = "syn"
+
version = "2.0.77"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "unicode-ident",
+
]
+

+
[[package]]
+
name = "sync_wrapper"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+

+
[[package]]
+
name = "sync_wrapper"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
+

+
[[package]]
+
name = "tar"
+
version = "0.4.41"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909"
+
dependencies = [
+
 "filetime",
+
 "libc",
+
 "xattr",
+
]
+

+
[[package]]
+
name = "tempfile"
+
version = "3.12.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
+
dependencies = [
+
 "cfg-if",
+
 "fastrand",
+
 "once_cell",
+
 "rustix",
+
 "windows-sys 0.59.0",
+
]
+

+
[[package]]
+
name = "termion"
+
version = "2.0.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c4648c7def6f2043b2568617b9f9b75eae88ca185dbc1f1fda30e95a85d49d7d"
+
dependencies = [
+
 "libc",
+
 "libredox 0.0.2",
+
 "numtoa",
+
 "redox_termios",
+
]
+

+
[[package]]
+
name = "termion"
+
version = "3.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "417813675a504dfbbf21bfde32c03e5bf9f2413999962b479023c02848c1c7a5"
+
dependencies = [
+
 "libc",
+
 "libredox 0.0.2",
+
 "numtoa",
+
 "redox_termios",
+
]
+

+
[[package]]
+
name = "thiserror"
+
version = "1.0.63"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
+
dependencies = [
+
 "thiserror-impl",
+
]
+

+
[[package]]
+
name = "thiserror-impl"
+
version = "1.0.63"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.77",
+
]
+

+
[[package]]
+
name = "thread_local"
+
version = "1.1.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+
dependencies = [
+
 "cfg-if",
+
 "once_cell",
+
]
+

+
[[package]]
+
name = "time"
+
version = "0.3.36"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
+
dependencies = [
+
 "deranged",
+
 "itoa",
+
 "num-conv",
+
 "powerfmt",
+
 "serde",
+
 "time-core",
+
 "time-macros",
+
]
+

+
[[package]]
+
name = "time-core"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+

+
[[package]]
+
name = "time-macros"
+
version = "0.2.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
+
dependencies = [
+
 "num-conv",
+
 "time-core",
+
]
+

+
[[package]]
+
name = "tinyvec"
+
version = "1.8.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
+
dependencies = [
+
 "tinyvec_macros",
+
]
+

+
[[package]]
+
name = "tinyvec_macros"
+
version = "0.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+

+
[[package]]
+
name = "tokio"
+
version = "1.40.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
+
dependencies = [
+
 "backtrace",
+
 "libc",
+
 "mio",
+
 "pin-project-lite",
+
 "socket2",
+
 "tokio-macros",
+
 "windows-sys 0.52.0",
+
]
+

+
[[package]]
+
name = "tokio-macros"
+
version = "2.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.77",
+
]
+

+
[[package]]
+
name = "tower"
+
version = "0.4.13"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+
dependencies = [
+
 "futures-core",
+
 "futures-util",
+
 "pin-project",
+
 "pin-project-lite",
+
 "tokio",
+
 "tower-layer",
+
 "tower-service",
+
]
+

+
[[package]]
+
name = "tower"
+
version = "0.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "36b837f86b25d7c0d7988f00a54e74739be6477f2aac6201b8f429a7569991b7"
+
dependencies = [
+
 "futures-core",
+
 "futures-util",
+
 "pin-project-lite",
+
 "sync_wrapper 0.1.2",
+
 "tower-layer",
+
 "tower-service",
+
]
+

+
[[package]]
+
name = "tower-http"
+
version = "0.5.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
+
dependencies = [
+
 "bitflags 2.6.0",
+
 "bytes",
+
 "http",
+
 "http-body",
+
 "http-body-util",
+
 "pin-project-lite",
+
 "tower-layer",
+
 "tower-service",
+
 "tracing",
+
]
+

+
[[package]]
+
name = "tower-layer"
+
version = "0.3.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+

+
[[package]]
+
name = "tower-service"
+
version = "0.3.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+

+
[[package]]
+
name = "tracing"
+
version = "0.1.40"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+
dependencies = [
+
 "log",
+
 "pin-project-lite",
+
 "tracing-attributes",
+
 "tracing-core",
+
]
+

+
[[package]]
+
name = "tracing-attributes"
+
version = "0.1.27"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.77",
+
]
+

+
[[package]]
+
name = "tracing-core"
+
version = "0.1.32"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+
dependencies = [
+
 "once_cell",
+
 "valuable",
+
]
+

+
[[package]]
+
name = "tracing-logfmt"
+
version = "0.3.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6b1f47d22deb79c3f59fcf2a1f00f60cbdc05462bf17d1cd356c1fefa3f444bd"
+
dependencies = [
+
 "time",
+
 "tracing",
+
 "tracing-core",
+
 "tracing-subscriber",
+
]
+

+
[[package]]
+
name = "tracing-subscriber"
+
version = "0.3.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+
dependencies = [
+
 "matchers",
+
 "nu-ansi-term",
+
 "once_cell",
+
 "regex",
+
 "sharded-slab",
+
 "thread_local",
+
 "tracing",
+
 "tracing-core",
+
]
+

+
[[package]]
+
name = "try-lock"
+
version = "0.2.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+

+
[[package]]
+
name = "typenum"
+
version = "1.17.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+

+
[[package]]
+
name = "unicode-bidi"
+
version = "0.3.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+

+
[[package]]
+
name = "unicode-display-width"
+
version = "0.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9a43273b656140aa2bb8e65351fe87c255f0eca706b2538a9bd4a590a3490bf3"
+
dependencies = [
+
 "unicode-segmentation",
+
]
+

+
[[package]]
+
name = "unicode-ident"
+
version = "1.0.12"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+

+
[[package]]
+
name = "unicode-normalization"
+
version = "0.1.23"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+
dependencies = [
+
 "tinyvec",
+
]
+

+
[[package]]
+
name = "unicode-segmentation"
+
version = "1.11.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
+

+
[[package]]
+
name = "unicode-width"
+
version = "0.1.13"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
+

+
[[package]]
+
name = "universal-hash"
+
version = "0.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+
dependencies = [
+
 "crypto-common",
+
 "subtle",
+
]
+

+
[[package]]
+
name = "url"
+
version = "2.5.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
+
dependencies = [
+
 "form_urlencoded",
+
 "idna",
+
 "percent-encoding",
+
 "serde",
+
]
+

+
[[package]]
+
name = "valuable"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+

+
[[package]]
+
name = "vcpkg"
+
version = "0.2.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+

+
[[package]]
+
name = "version_check"
+
version = "0.9.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+

+
[[package]]
+
name = "want"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+
dependencies = [
+
 "try-lock",
+
]
+

+
[[package]]
+
name = "wasi"
+
version = "0.11.0+wasi-snapshot-preview1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+

+
[[package]]
+
name = "wasm-bindgen"
+
version = "0.2.93"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
+
dependencies = [
+
 "cfg-if",
+
 "once_cell",
+
 "wasm-bindgen-macro",
+
]
+

+
[[package]]
+
name = "wasm-bindgen-backend"
+
version = "0.2.93"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
+
dependencies = [
+
 "bumpalo",
+
 "log",
+
 "once_cell",
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.77",
+
 "wasm-bindgen-shared",
+
]
+

+
[[package]]
+
name = "wasm-bindgen-macro"
+
version = "0.2.93"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
+
dependencies = [
+
 "quote",
+
 "wasm-bindgen-macro-support",
+
]
+

+
[[package]]
+
name = "wasm-bindgen-macro-support"
+
version = "0.2.93"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.77",
+
 "wasm-bindgen-backend",
+
 "wasm-bindgen-shared",
+
]
+

+
[[package]]
+
name = "wasm-bindgen-shared"
+
version = "0.2.93"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
+

+
[[package]]
+
name = "winapi"
+
version = "0.3.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+
dependencies = [
+
 "winapi-i686-pc-windows-gnu",
+
 "winapi-x86_64-pc-windows-gnu",
+
]
+

+
[[package]]
+
name = "winapi-i686-pc-windows-gnu"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+

+
[[package]]
+
name = "winapi-x86_64-pc-windows-gnu"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+

+
[[package]]
+
name = "windows-core"
+
version = "0.52.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+
dependencies = [
+
 "windows-targets",
+
]
+

+
[[package]]
+
name = "windows-sys"
+
version = "0.52.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+
dependencies = [
+
 "windows-targets",
+
]
+

+
[[package]]
+
name = "windows-sys"
+
version = "0.59.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+
dependencies = [
+
 "windows-targets",
+
]
+

+
[[package]]
+
name = "windows-targets"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+
dependencies = [
+
 "windows_aarch64_gnullvm",
+
 "windows_aarch64_msvc",
+
 "windows_i686_gnu",
+
 "windows_i686_gnullvm",
+
 "windows_i686_msvc",
+
 "windows_x86_64_gnu",
+
 "windows_x86_64_gnullvm",
+
 "windows_x86_64_msvc",
+
]
+

+
[[package]]
+
name = "windows_aarch64_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+

+
[[package]]
+
name = "windows_aarch64_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+

+
[[package]]
+
name = "windows_i686_gnu"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+

+
[[package]]
+
name = "windows_i686_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+

+
[[package]]
+
name = "windows_i686_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+

+
[[package]]
+
name = "windows_x86_64_gnu"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+

+
[[package]]
+
name = "windows_x86_64_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+

+
[[package]]
+
name = "windows_x86_64_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+

+
[[package]]
+
name = "xattr"
+
version = "1.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f"
+
dependencies = [
+
 "libc",
+
 "linux-raw-sys",
+
 "rustix",
+
]
+

+
[[package]]
+
name = "yansi"
+
version = "0.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
+

+
[[package]]
+
name = "zerocopy"
+
version = "0.7.35"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+
dependencies = [
+
 "byteorder",
+
 "zerocopy-derive",
+
]
+

+
[[package]]
+
name = "zerocopy-derive"
+
version = "0.7.35"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.77",
+
]
+

+
[[package]]
+
name = "zeroize"
+
version = "1.8.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
added crates/test-http-api/Cargo.toml
@@ -0,0 +1,20 @@
+
[package]
+
name = "test-http-api"
+
description = "HTTP Test API"
+
homepage = "https://radicle.xyz"
+
version = "0.1.0"
+
edition = "2021"
+

+
[dependencies]
+
anyhow = { version = "1.0.90" }
+
axum = { version = "0.7.5", default-features = false, features = ["json", "query", "tokio", "http1"] }
+
hyper = { version = "1.4", default-features = false }
+
lexopt = { version = "0.3.0" }
+
radicle = { git = "https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git" }
+
radicle-surf = { version = "0.22.1", default-features = false, features = ["serde"] }
+
radicle-types = { path = "../radicle-types" }
+
serde = { version = "1", features = ["derive"] }
+
serde_json = { version = "1", features = ["preserve_order"] }
+
thiserror = { version = "1" }
+
tokio = { version = "1.40", default-features = false, features = ["macros", "rt-multi-thread"] }
+
tower-http = { version = "0.5.2", default-features = false, features = ["cors", "set-header"] }
added crates/test-http-api/src/api.rs
@@ -0,0 +1,306 @@
+
use std::ops::Deref;
+
use std::sync::Arc;
+

+
use axum::extract::State;
+
use axum::response::{IntoResponse, Json};
+
use axum::routing::post;
+
use axum::Router;
+
use hyper::header::CONTENT_TYPE;
+
use hyper::Method;
+
use radicle::cob::TypeName;
+
use serde::{Deserialize, Serialize};
+
use tower_http::cors::{self, CorsLayer};
+

+
use radicle::{git, identity};
+
use radicle_types as types;
+
use radicle_types::cobs::issue::{Action, NewIssue};
+
use radicle_types::cobs::CobOptions;
+
use radicle_types::traits::auth::Auth;
+
use radicle_types::traits::cobs::Cobs;
+
use radicle_types::traits::issue::{Issues, IssuesMut};
+
use radicle_types::traits::patch::{Patches, PatchesMut};
+
use radicle_types::traits::repo::Repo;
+
use radicle_types::traits::thread::Thread;
+
use radicle_types::traits::Profile;
+

+
use crate::error::Error;
+

+
#[derive(Clone)]
+
pub struct Context {
+
    profile: Arc<radicle::Profile>,
+
}
+

+
impl Auth for Context {}
+
impl Repo for Context {}
+
impl Cobs for Context {}
+
impl Thread for Context {}
+
impl Issues for Context {}
+
impl IssuesMut for Context {}
+
impl Patches for Context {}
+
impl PatchesMut for Context {}
+
impl Profile for Context {
+
    fn profile(&self) -> radicle::Profile {
+
        self.profile.deref().clone()
+
    }
+
}
+

+
impl Context {
+
    pub fn new(profile: Arc<radicle::Profile>) -> Self {
+
        Self { profile }
+
    }
+
}
+

+
pub fn router(ctx: Context) -> Router {
+
    Router::new()
+
        .route("/config", post(config_handler))
+
        .route("/authenticate", post(auth_handler))
+
        .route("/list_repos", post(repo_root_handler))
+
        .route("/repo_by_id", post(repo_handler))
+
        .route("/diff_stats", post(diff_stats_handler))
+
        .route("/activity_by_id", post(activity_handler))
+
        .route("/get_diff", post(diff_handler))
+
        .route("/list_issues", post(issues_handler))
+
        .route("/create_issue", post(create_issue_handler))
+
        .route("/edit_issue", post(edit_issue_handler))
+
        .route("/issue_by_id", post(issue_handler))
+
        .route("/list_patches", post(patches_handler))
+
        .route("/patch_by_id", post(patch_handler))
+
        .route("/revisions_by_patch", post(revision_handler))
+
        .route("/get_file_by_oid", post(get_embeds_handler))
+
        .route("/save_embed", post(save_embed_handler))
+
        .layer(
+
            CorsLayer::new()
+
                .allow_origin(cors::Any)
+
                .allow_methods([Method::POST, Method::GET])
+
                .allow_headers([CONTENT_TYPE]),
+
        )
+
        .with_state(ctx)
+
}
+

+
async fn config_handler(State(ctx): State<Context>) -> impl IntoResponse {
+
    let config = ctx.config();
+

+
    Ok::<_, Error>(Json(config))
+
}
+

+
async fn auth_handler(State(ctx): State<Context>) -> impl IntoResponse {
+
    ctx.authenticate()?;
+

+
    Ok::<_, Error>(Json(()))
+
}
+

+
async fn repo_root_handler(State(ctx): State<Context>) -> impl IntoResponse {
+
    let repos = ctx.list_repos()?;
+

+
    Ok::<_, Error>(Json(repos))
+
}
+

+
#[derive(Serialize, Deserialize)]
+
struct RepoBody {
+
    pub rid: identity::RepoId,
+
}
+

+
async fn repo_handler(
+
    State(ctx): State<Context>,
+
    Json(RepoBody { rid }): Json<RepoBody>,
+
) -> impl IntoResponse {
+
    let info = ctx.repo_by_id(rid)?;
+

+
    Ok::<_, Error>(Json(info))
+
}
+

+
#[derive(Serialize, Deserialize)]
+
struct DiffStatsBody {
+
    pub rid: identity::RepoId,
+
    pub base: git::Oid,
+
    pub head: git::Oid,
+
}
+

+
async fn diff_stats_handler(
+
    State(ctx): State<Context>,
+
    Json(DiffStatsBody { rid, base, head }): Json<DiffStatsBody>,
+
) -> impl IntoResponse {
+
    let info = ctx.diff_stats(rid, base, head)?;
+

+
    Ok::<_, Error>(Json(info))
+
}
+

+
#[derive(Serialize, Deserialize)]
+
struct DiffBody {
+
    pub rid: identity::RepoId,
+
    pub options: types::cobs::diff::Options,
+
}
+

+
async fn diff_handler(
+
    State(ctx): State<Context>,
+
    Json(DiffBody { rid, options }): Json<DiffBody>,
+
) -> impl IntoResponse {
+
    let info = ctx.get_diff(rid, options)?;
+

+
    Ok::<_, Error>(Json(info))
+
}
+

+
#[derive(Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
struct IssuesBody {
+
    pub rid: identity::RepoId,
+
    pub status: Option<types::cobs::query::IssueStatus>,
+
}
+

+
async fn issues_handler(
+
    State(ctx): State<Context>,
+
    Json(IssuesBody { rid, status }): Json<IssuesBody>,
+
) -> impl IntoResponse {
+
    let issues = ctx.list_issues(rid, status)?;
+

+
    Ok::<_, Error>(Json(issues))
+
}
+

+
#[derive(Serialize, Deserialize)]
+
struct CreateIssuesBody {
+
    pub rid: identity::RepoId,
+
    pub opts: CobOptions,
+
    pub new: NewIssue,
+
}
+

+
async fn create_issue_handler(
+
    State(ctx): State<Context>,
+
    Json(CreateIssuesBody { rid, opts, new }): Json<CreateIssuesBody>,
+
) -> impl IntoResponse {
+
    let issues = ctx.create_issue(rid, new, opts)?;
+

+
    Ok::<_, Error>(Json(issues))
+
}
+

+
#[derive(Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
struct EditIssuesBody {
+
    pub rid: identity::RepoId,
+
    pub cob_id: git::Oid,
+
    pub action: Action,
+
    pub opts: CobOptions,
+
}
+

+
async fn edit_issue_handler(
+
    State(ctx): State<Context>,
+
    Json(EditIssuesBody {
+
        rid,
+
        cob_id,
+
        action,
+
        opts,
+
    }): Json<EditIssuesBody>,
+
) -> impl IntoResponse {
+
    let issues = ctx.edit_issue(rid, cob_id, action, opts)?;
+

+
    Ok::<_, Error>(Json(issues))
+
}
+

+
#[derive(Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
struct ActivityBody {
+
    pub rid: identity::RepoId,
+
    pub type_name: TypeName,
+
    pub id: git::Oid,
+
}
+

+
async fn activity_handler(
+
    State(ctx): State<Context>,
+
    Json(ActivityBody { rid, type_name, id }): Json<ActivityBody>,
+
) -> impl IntoResponse {
+
    let activity = ctx.activity_by_id(rid, type_name, id)?;
+

+
    Ok::<_, Error>(Json(activity))
+
}
+

+
#[derive(Serialize, Deserialize)]
+
struct EmbedBody {
+
    pub rid: identity::RepoId,
+
    pub oid: git::Oid,
+
}
+

+
async fn get_embeds_handler(
+
    State(ctx): State<Context>,
+
    Json(EmbedBody { rid, oid }): Json<EmbedBody>,
+
) -> impl IntoResponse {
+
    let embed = ctx.get_embed(rid, oid)?;
+

+
    Ok::<_, Error>(Json(embed))
+
}
+

+
#[derive(Serialize, Deserialize)]
+
struct CreateEmbedBody {
+
    pub rid: identity::RepoId,
+
    pub name: String,
+
    pub content: Vec<u8>,
+
}
+

+
async fn save_embed_handler(
+
    State(ctx): State<Context>,
+
    Json(CreateEmbedBody { rid, name, content }): Json<CreateEmbedBody>,
+
) -> impl IntoResponse {
+
    let embed = ctx.save_embed(rid, &name, &content)?;
+

+
    Ok::<_, Error>(Json(embed))
+
}
+

+
#[derive(Serialize, Deserialize)]
+
struct IssueBody {
+
    pub rid: identity::RepoId,
+
    pub id: git::Oid,
+
}
+

+
async fn issue_handler(
+
    State(ctx): State<Context>,
+
    Json(IssueBody { rid, id }): Json<IssueBody>,
+
) -> impl IntoResponse {
+
    let issue = ctx.issue_by_id(rid, id)?;
+

+
    Ok::<_, Error>(Json(issue))
+
}
+

+
#[derive(Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
struct PatchesBody {
+
    pub rid: identity::RepoId,
+
    pub page: Option<usize>,
+
    pub per_page: Option<usize>,
+
    pub status: Option<types::cobs::query::PatchStatus>,
+
}
+

+
async fn patches_handler(
+
    State(ctx): State<Context>,
+
    Json(PatchesBody {
+
        rid,
+
        page,
+
        per_page,
+
        status,
+
    }): Json<PatchesBody>,
+
) -> impl IntoResponse {
+
    let patches = ctx.list_patches(rid, status, page, per_page)?;
+

+
    Ok::<_, Error>(Json(patches))
+
}
+

+
#[derive(Serialize, Deserialize)]
+
struct PatchBody {
+
    pub rid: identity::RepoId,
+
    pub id: git::Oid,
+
}
+

+
async fn patch_handler(
+
    State(ctx): State<Context>,
+
    Json(PatchBody { rid, id }): Json<PatchBody>,
+
) -> impl IntoResponse {
+
    let patch = ctx.get_patch(rid, id)?;
+

+
    Ok::<_, Error>(Json(patch))
+
}
+

+
async fn revision_handler(
+
    State(ctx): State<Context>,
+
    Json(PatchBody { rid, id }): Json<PatchBody>,
+
) -> impl IntoResponse {
+
    let revisions = ctx.revisions_by_patch(rid, id)?;
+

+
    Ok::<_, Error>(Json(revisions))
+
}
added crates/test-http-api/src/error.rs
@@ -0,0 +1,32 @@
+
use axum::http::StatusCode;
+
use axum::response::{IntoResponse, Response};
+
use axum::Json;
+
use serde_json::json;
+

+
#[derive(Debug, thiserror::Error)]
+
pub enum Error {
+
    /// radicle_types error.
+
    #[error(transparent)]
+
    Types(#[from] radicle_types::error::Error),
+
}
+

+
impl IntoResponse for Error {
+
    fn into_response(self) -> Response {
+
        let (status, msg) = match self {
+
            other => {
+
                if cfg!(debug_assertions) {
+
                    (StatusCode::INTERNAL_SERVER_ERROR, Some(other.to_string()))
+
                } else {
+
                    (StatusCode::INTERNAL_SERVER_ERROR, None)
+
                }
+
            }
+
        };
+

+
        let body = Json(json!({
+
            "error": msg.or_else(|| status.canonical_reason().map(|r| r.to_string())),
+
            "code": status.as_u16()
+
        }));
+

+
        (status, body).into_response()
+
    }
+
}
added crates/test-http-api/src/lib.rs
@@ -0,0 +1,32 @@
+
use std::net::SocketAddr;
+
use std::sync::Arc;
+

+
use axum::Router;
+
use tokio::net::TcpListener;
+

+
use radicle::Profile;
+

+
mod api;
+
mod error;
+

+
#[derive(Debug, Clone)]
+
pub struct Options {
+
    pub listen: SocketAddr,
+
}
+

+
pub async fn run(options: Options) -> anyhow::Result<()> {
+
    let profile = Profile::load()?;
+
    let listener = TcpListener::bind(options.listen).await?;
+
    let app = router(profile)?.into_make_service_with_connect_info::<SocketAddr>();
+

+
    axum::serve(listener, app)
+
        .await
+
        .map_err(anyhow::Error::from)
+
}
+

+
fn router(profile: Profile) -> anyhow::Result<Router> {
+
    let profile = Arc::new(profile);
+
    let ctx = api::Context::new(profile);
+

+
    Ok(api::router(ctx))
+
}
added crates/test-http-api/src/main.rs
@@ -0,0 +1,35 @@
+
use std::process;
+

+
use test_http_api as api;
+

+
#[tokio::main]
+
async fn main() -> anyhow::Result<()> {
+
    let options = parse_options()?;
+
    match api::run(options).await {
+
        Ok(()) => {}
+
        Err(_) => {
+
            process::exit(1);
+
        }
+
    }
+
    Ok(())
+
}
+

+
fn parse_options() -> Result<api::Options, lexopt::Error> {
+
    use lexopt::prelude::*;
+

+
    let mut parser = lexopt::Parser::from_env();
+
    let mut listen = None;
+

+
    while let Some(arg) = parser.next()? {
+
        match arg {
+
            Long("listen") => {
+
                let addr = parser.value()?.parse()?;
+
                listen = Some(addr);
+
            }
+
            _ => return Err(arg.unexpected()),
+
        }
+
    }
+
    Ok(api::Options {
+
        listen: listen.unwrap_or_else(|| ([0, 0, 0, 0], 8080).into()),
+
    })
+
}
modified package.json
@@ -6,6 +6,7 @@
  "type": "module",
  "scripts": {
    "start": "vite",
+
    "start:http": "cargo run --manifest-path ./crates/test-http-api/Cargo.toml",
    "build": "vite build && scripts/copy-katex-assets && scripts/install-twemoji-assets",
    "postinstall": "scripts/copy-katex-assets && scripts/install-twemoji-assets",
    "preview": "vite preview",
modified src/App.svelte
@@ -3,7 +3,7 @@

  import { onDestroy, onMount } from "svelte";

-
  import { invoke } from "@tauri-apps/api/core";
+
  import { invoke } from "@app/lib/invoke";
  import { listen } from "@tauri-apps/api/event";

  import * as router from "@app/lib/router";
@@ -25,13 +25,15 @@
  let unlistenNodeEvents: UnlistenFn | undefined = undefined;

  onMount(async () => {
-
    unlistenEvents = await listen("event", event => {
-
      console.log(event.payload);
-
    });
+
    if (window.__TAURI_INTERNALS__) {
+
      unlistenEvents = await listen("event", event => {
+
        console.log(event.payload);
+
      });

-
    unlistenNodeEvents = await listen<boolean>("node_running", event => {
-
      nodeRunning.set(event.payload);
-
    });
+
      unlistenNodeEvents = await listen<boolean>("node_running", event => {
+
        nodeRunning.set(event.payload);
+
      });
+
    }

    try {
      await invoke("authenticate");
modified src/components/Markdown.svelte
@@ -7,7 +7,7 @@
  import { Renderer, markdownWithExtensions } from "@app/lib/markdown";
  import { highlight } from "@app/lib/syntax";
  import { twemoji, scrollIntoView, isCommit } from "@app/lib/utils";
-
  import { invoke } from "@tauri-apps/api/core";
+
  import { invoke } from "@app/lib/invoke";

  export let rid: string;
  export let content: string;
modified src/components/PatchTeaser.svelte
@@ -8,7 +8,7 @@
    patchStatusBackgroundColor,
    patchStatusColor,
  } from "@app/lib/utils";
-
  import { invoke } from "@tauri-apps/api/core";
+
  import { invoke } from "@app/lib/invoke";
  import { onMount } from "svelte";
  import { push } from "@app/lib/router";

added src/lib/invoke.ts
@@ -0,0 +1,21 @@
+
import * as tauri from "@tauri-apps/api/core";
+

+
export async function invoke<T = null>(
+
  cmd: string,
+
  args?: tauri.InvokeArgs,
+
  options?: tauri.InvokeOptions,
+
): Promise<T> {
+
  if (window.__TAURI_INTERNALS__) {
+
    return tauri.invoke(cmd, args, options);
+
  } else {
+
    return fetch(`http://127.0.0.1:8080/${cmd}`, {
+
      method: "POST",
+
      headers: { "Content-Type": "application/json" },
+
      body: JSON.stringify(args),
+
    })
+
      .then(data => data.json())
+
      .catch(() => {
+
        throw Error(`Issue with HTTP Route: ${JSON.stringify({ cmd, args })}`);
+
      });
+
  }
+
}
modified src/lib/router/definitions.ts
@@ -2,7 +2,7 @@ import type { Config } from "@bindings/config/Config";
import type { RepoInfo } from "@bindings/repo/RepoInfo";
import type { LoadedRepoRoute, RepoRoute } from "@app/views/repo/router";

-
import { invoke } from "@tauri-apps/api/core";
+
import { invoke } from "@app/lib/invoke";

import {
  loadCreateIssue,
modified src/views/repo/CreateIssue.svelte
@@ -4,7 +4,7 @@
  import type { Issue } from "@bindings/cob/issue/Issue";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

-
  import { invoke } from "@tauri-apps/api/core";
+
  import { invoke } from "@app/lib/invoke";

  import { issueStatusColor } from "@app/lib/utils";
  import * as router from "@app/lib/router";
modified src/views/repo/Issue.svelte
@@ -14,7 +14,7 @@
    publicKeyFromDid,
    scrollIntoView,
  } from "@app/lib/utils";
-
  import { invoke } from "@tauri-apps/api/core";
+
  import { invoke } from "@app/lib/invoke";

  import { announce } from "@app/components/AnnounceSwitch.svelte";

modified src/views/repo/Patch.svelte
@@ -9,7 +9,7 @@
    formatTimestamp,
    patchStatusColor,
  } from "@app/lib/utils";
-
  import { invoke } from "@tauri-apps/api/core";
+
  import { invoke } from "@app/lib/invoke";

  import Border from "@app/components/Border.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
modified src/views/repo/Patches.svelte
@@ -5,7 +5,7 @@
  import type { PatchStatus } from "./router";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

-
  import { invoke } from "@tauri-apps/api/core";
+
  import { invoke } from "@app/lib/invoke";

  import Layout from "./Layout.svelte";
  import Border from "@app/components/Border.svelte";
modified src/views/repo/router.ts
@@ -5,7 +5,7 @@ import type { Patch } from "@bindings/cob/patch/Patch";
import type { RepoInfo } from "@bindings/repo/RepoInfo";
import type { Revision } from "@bindings/cob/patch/Revision";

-
import { invoke } from "@tauri-apps/api/core";
+
import { invoke } from "@app/lib/invoke";
import { unreachable } from "@app/lib/utils";

export type IssueStatus = "all" | Issue["state"]["status"];