OXYGENHUB

HTTP transport: CBOR wire messages, HttpRemote client, oxy-server crate with bearer-token pushes, oxy serve 750251b21a96

Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-26 18:47 UTC
Signature
✔ signed by Andy McCall
Hash
750251b21a96e27ed790cb825a7cc67a5967c07a749eae6f6a3f8a3361045beb
Depends on
1983686fb7a7 20247a03a596 5b9c6ad45f6e e993a086c13f ff0af1ee299d
Depended on by
835ceb9b3ea7 135bdcadec4a ed931d1d1102 0588de69a84d 569802ebae0e 4275ecb110f5 642fa213de80 80b069facc69 e02edccce98c e30c081a5064 b260109d27c5 f3811eb20bea d3b270909c78 b63fc7a77fc6 82202a4d0025 ee130dd9eef2 1ec6fdbc6bcb 6f06d9f912b4 67a4fe1aa2d5 028545dfa7a4 51d4862a16d8 4b9d06778581 68dcf21fe055 f05b84d1ad2d f53cbe7a204e f0a517d28237 efad40f9ac8e 68a924846379 e2a5edbf1395 cb8b8082fa73 d0bf2da15094 579afd107b9b c71a5777509a 2cd40ea1c04c f874dc8659c2 8d80356371c5 9f9e72b93e4e 6a0008781d5e bea00d4177c6 827281eed2ed b0461604ed98 14edbaaf5291 ff1f62fee654 2c75672fb2fe 598009a75f6c 6f8aeb996558 be451356e3ef 8c30d814f2cc f8c695764c95 11c958021e1a d42369abf180 3e5772ccf9da 37c299cec358 d82e44097323 be0161a88dd7 76f752503e7e 119bbf50cb86 7aa2dc9369e3 b0e64e81e4fb 05e71ed6cdf1 75167672ebcd e02bf0cd0533 c6aa5bd9d1c8 b3c5a59a875a 2b872d074ddb 28fc23d7706a 132b884b9a8d 2863456a0f6d 7421309f5372 135212de5370 d02e545dc265 83e0677b07fe 2b5ee371060e 982864068a59 fce44ae9ad29 02f1b14af1e2
In bookmarks
agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
In tags
v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
71 atoms, 6 tree operations, 9 files

Browse the tree at this change

added crates/oxy-server/Cargo.toml +14 −0
1 +
[package]
2 +
name = "oxy-server"
3 +
version.workspace = true
4 +
edition.workspace = true
5 +
license.workspace = true
6 +
description = "Reference HTTP server for the Oxygen wire protocol"
7 +
8 +
[dependencies]
9 +
oxy-core = { path = "../oxy-core" }
10 +
axum = "0.8"
11 +
tokio = { version = "1", features = ["rt-multi-thread", "net", "macros", "sync"] }
12 +
13 +
[dev-dependencies]
14 +
tempfile = "3"
added crates/oxy-server/src/lib.rs +166 −0
1 +
//! Reference HTTP server for the Oxygen wire protocol (spec §15.1, §16).
2 +
//!
3 +
//! Serves every repository directory under a root at `/{repo}/oxy/v1/...`.
4 +
//! TLS is expected to be terminated in front of this server for now; the
5 +
//! protocol's integrity does not depend on the channel (spec §15).
6 +
//!
7 +
//! Authentication: when bearer tokens are configured, `push` requires one;
8 +
//! reads are open. With no tokens configured everything is open (development).
9 +
10 +
use axum::body::Bytes;
11 +
use axum::extract::{Path, State};
12 +
use axum::http::{header, HeaderMap, StatusCode};
13 +
use axum::response::{IntoResponse, Response};
14 +
use axum::routing::{get, post};
15 +
use axum::Router;
16 +
use oxy_core::protocol::{wire, CONTENT_TYPE};
17 +
use oxy_core::repo::Repo;
18 +
use std::collections::HashMap;
19 +
use std::path::PathBuf;
20 +
use std::sync::{Arc, Mutex};
21 +
22 +
#[derive(Clone, Debug)]
23 +
pub struct ServerConfig {
24 +
    /// Directory containing one repository per subdirectory.
25 +
    pub root: PathBuf,
26 +
    /// Bearer tokens allowed to push; empty means no authentication.
27 +
    pub tokens: Vec<String>,
28 +
}
29 +
30 +
struct App {
31 +
    cfg: ServerConfig,
32 +
    /// One lock per repository so concurrent pushes serialize.
33 +
    locks: Mutex<HashMap<String, Arc<Mutex<()>>>>,
34 +
}
35 +
36 +
type Shared = Arc<App>;
37 +
38 +
struct Failure(StatusCode, String);
39 +
40 +
impl IntoResponse for Failure {
41 +
    fn into_response(self) -> Response {
42 +
        (self.0, self.1).into_response()
43 +
    }
44 +
}
45 +
46 +
impl From<oxy_core::repo::RepoError> for Failure {
47 +
    fn from(e: oxy_core::repo::RepoError) -> Failure {
48 +
        Failure(StatusCode::BAD_REQUEST, e.to_string())
49 +
    }
50 +
}
51 +
52 +
fn ok(body: Vec<u8>) -> Response {
53 +
    ([(header::CONTENT_TYPE, CONTENT_TYPE)], body).into_response()
54 +
}
55 +
56 +
fn valid_repo_name(name: &str) -> bool {
57 +
    !name.is_empty()
58 +
        && !name.starts_with('.')
59 +
        && name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
60 +
}
61 +
62 +
impl App {
63 +
    fn open(&self, name: &str) -> Result<Repo, Failure> {
64 +
        if !valid_repo_name(name) {
65 +
            return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into()));
66 +
        }
67 +
        let dir = self.cfg.root.join(name);
68 +
        if !dir.join(".oxy").is_dir() {
69 +
            return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into()));
70 +
        }
71 +
        Repo::open(&dir).map_err(|e| Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
72 +
    }
73 +
74 +
    fn lock_for(&self, name: &str) -> Arc<Mutex<()>> {
75 +
        self.locks.lock().unwrap().entry(name.to_string()).or_default().clone()
76 +
    }
77 +
78 +
    fn authorize_push(&self, headers: &HeaderMap) -> Result<(), Failure> {
79 +
        if self.cfg.tokens.is_empty() {
80 +
            return Ok(());
81 +
        }
82 +
        let presented = headers
83 +
            .get(header::AUTHORIZATION)
84 +
            .and_then(|v| v.to_str().ok())
85 +
            .and_then(|v| v.strip_prefix("Bearer "))
86 +
            .map(str::trim);
87 +
        match presented {
88 +
            Some(t) if self.cfg.tokens.iter().any(|x| x == t) => Ok(()),
89 +
            _ => Err(Failure(StatusCode::UNAUTHORIZED, "a valid bearer token is required to push".into())),
90 +
        }
91 +
    }
92 +
}
93 +
94 +
async fn blocking<T: Send + 'static>(f: impl FnOnce() -> Result<T, Failure> + Send + 'static) -> Result<T, Failure> {
95 +
    tokio::task::spawn_blocking(f).await.map_err(|e| Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
96 +
}
97 +
98 +
async fn capabilities(State(app): State<Shared>, Path(repo): Path<String>) -> Result<Response, Failure> {
99 +
    blocking(move || {
100 +
        let r = app.open(&repo)?;
101 +
        Ok(ok(wire::encode_capabilities(r.id()?)))
102 +
    })
103 +
    .await
104 +
}
105 +
106 +
async fn refs(State(app): State<Shared>, Path(repo): Path<String>) -> Result<Response, Failure> {
107 +
    blocking(move || {
108 +
        let r = app.open(&repo)?;
109 +
        Ok(ok(wire::encode_refs(r.id()?, &r.remote_refs()?)))
110 +
    })
111 +
    .await
112 +
}
113 +
114 +
async fn negotiate(State(app): State<Shared>, Path(repo): Path<String>, body: Bytes) -> Result<Response, Failure> {
115 +
    blocking(move || {
116 +
        let r = app.open(&repo)?;
117 +
        let (have, want) = wire::decode_negotiate(&body)?;
118 +
        Ok(ok(wire::encode_hashes(&r.negotiate(&have, &want)?)))
119 +
    })
120 +
    .await
121 +
}
122 +
123 +
async fn fetch(State(app): State<Shared>, Path(repo): Path<String>, body: Bytes) -> Result<Response, Failure> {
124 +
    blocking(move || {
125 +
        let r = app.open(&repo)?;
126 +
        let changes = wire::decode_hashes(&body)?;
127 +
        Ok(ok(wire::encode_bundle(&r.bundle_for(&changes)?)))
128 +
    })
129 +
    .await
130 +
}
131 +
132 +
async fn push(State(app): State<Shared>, Path(repo): Path<String>, headers: HeaderMap, body: Bytes) -> Result<Response, Failure> {
133 +
    app.authorize_push(&headers)?;
134 +
    blocking(move || {
135 +
        let r = app.open(&repo)?;
136 +
        let (bundle, updates) = wire::decode_push(&body)?;
137 +
        let lock = app.lock_for(&repo);
138 +
        let _guard = lock.lock().unwrap();
139 +
        Ok(ok(wire::encode_push_response(&r.receive_push(bundle, updates)?)))
140 +
    })
141 +
    .await
142 +
}
143 +
144 +
pub fn router(cfg: ServerConfig) -> Router {
145 +
    let app: Shared = Arc::new(App { cfg, locks: Mutex::new(HashMap::new()) });
146 +
    Router::new()
147 +
        .route("/{repo}/oxy/v1/capabilities", get(capabilities))
148 +
        .route("/{repo}/oxy/v1/refs", get(refs))
149 +
        .route("/{repo}/oxy/v1/negotiate", post(negotiate))
150 +
        .route("/{repo}/oxy/v1/fetch", post(fetch))
151 +
        .route("/{repo}/oxy/v1/push", post(push))
152 +
        .with_state(app)
153 +
}
154 +
155 +
/// Serve on an already-bound listener until the process ends.
156 +
pub async fn serve(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> {
157 +
    listener.set_nonblocking(true)?;
158 +
    let listener = tokio::net::TcpListener::from_std(listener)?;
159 +
    axum::serve(listener, router(cfg)).await
160 +
}
161 +
162 +
/// Build a runtime and serve; for the CLI and for tests on a background thread.
163 +
pub fn run_blocking(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> {
164 +
    let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?;
165 +
    rt.block_on(serve(listener, cfg))
166 +
}
added crates/oxy-server/tests/http.rs +89 −0
1 +
//! End-to-end: a real server on a loopback port, clients over HTTP.
2 +
3 +
use oxy_core::repo::{Repo, SignatureStatus};
4 +
use std::fs;
5 +
use std::net::TcpListener;
6 +
use std::path::Path;
7 +
8 +
fn write(root: &Path, rel: &str, content: &[u8]) {
9 +
    let p = root.join(rel);
10 +
    fs::create_dir_all(p.parent().unwrap()).unwrap();
11 +
    fs::write(p, content).unwrap();
12 +
}
13 +
14 +
fn start(root: &Path, tokens: Vec<String>) -> String {
15 +
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
16 +
    let port = listener.local_addr().unwrap().port();
17 +
    let cfg = oxy_server::ServerConfig { root: root.to_path_buf(), tokens };
18 +
    std::thread::spawn(move || oxy_server::run_blocking(listener, cfg).unwrap());
19 +
    format!("http://127.0.0.1:{port}")
20 +
}
21 +
22 +
#[test]
23 +
fn clone_push_pull_over_http() {
24 +
    let dir = tempfile::tempdir().unwrap();
25 +
    let root = dir.path();
26 +
    let served = root.join("served");
27 +
    Repo::init(&served.join("origin")).unwrap();
28 +
    let base = start(&served, vec![]);
29 +
    let url = format!("{base}/origin");
30 +
31 +
    let mut a = Repo::clone(&url, &root.join("a")).unwrap().with_keys_dir(root.join("keys"));
32 +
    let (key, _) = a.create_identity("Ada", "[email protected]").unwrap();
33 +
    write(&root.join("a"), "f.txt", b"hello\n");
34 +
    let h1 = a.commit("hello", &[]).unwrap();
35 +
    let r = a.push("origin", false).unwrap();
36 +
    assert!(r[0].ok, "{}", r[0].reason);
37 +
38 +
    let origin = Repo::open(&served.join("origin")).unwrap();
39 +
    assert_eq!(origin.bookmark_heads().unwrap(), vec![h1]);
40 +
    assert!(matches!(origin.authenticate_change(&h1).unwrap(), SignatureStatus::Valid { key: k, sealed: true, .. } if k == key));
41 +
42 +
    let b = Repo::clone(&url, &root.join("b")).unwrap();
43 +
    assert_eq!(fs::read(root.join("b/f.txt")).unwrap(), b"hello\n");
44 +
    write(&root.join("b"), "g.txt", b"g\n");
45 +
    let h2 = b.commit("g", &[]).unwrap();
46 +
    assert!(b.push("origin", false).unwrap()[0].ok);
47 +
48 +
    let pr = a.pull("origin").unwrap();
49 +
    assert!(pr.changed && pr.checked_out);
50 +
    assert_eq!(fs::read(root.join("a/g.txt")).unwrap(), b"g\n");
51 +
    let mut heads = a.bookmark_heads().unwrap();
52 +
    heads.sort();
53 +
    let mut want = vec![h1, h2];
54 +
    want.sort();
55 +
    assert_eq!(heads, want);
56 +
57 +
    // Unknown repositories and names the server refuses are surfaced as errors.
58 +
    let err = Repo::clone(&format!("{base}/nope"), &root.join("c")).unwrap_err().to_string();
59 +
    assert!(err.contains("404"), "{err}");
60 +
    fs::create_dir_all(served.join(".hidden/.oxy")).unwrap();
61 +
    assert!(Repo::clone(&format!("{base}/.hidden"), &root.join("d")).unwrap_err().to_string().contains("404"));
62 +
}
63 +
64 +
#[test]
65 +
fn push_requires_a_token_when_configured() {
66 +
    let dir = tempfile::tempdir().unwrap();
67 +
    let root = dir.path();
68 +
    let served = root.join("served");
69 +
    Repo::init(&served.join("origin")).unwrap();
70 +
    let base = start(&served, vec!["s3cret".into()]);
71 +
    let url = format!("{base}/origin");
72 +
73 +
    // Reads are open.
74 +
    let mut a = Repo::clone(&url, &root.join("a")).unwrap();
75 +
    write(&root.join("a"), "f.txt", b"x\n");
76 +
    a.commit("x", &[]).unwrap();
77 +
    let err = a.push("origin", false).unwrap_err().to_string();
78 +
    assert!(err.contains("401"), "{err}");
79 +
80 +
    a.config.set("remote.origin.token", "wrong").unwrap();
81 +
    a.save_config().unwrap();
82 +
    assert!(a.push("origin", false).unwrap_err().to_string().contains("401"));
83 +
84 +
    a.config.set("remote.origin.token", "s3cret").unwrap();
85 +
    a.save_config().unwrap();
86 +
    assert!(a.push("origin", false).unwrap()[0].ok);
87 +
    let origin = Repo::open(&served.join("origin")).unwrap();
88 +
    assert_eq!(origin.bookmark_heads().unwrap().len(), 1);
89 +
}
modified Cargo.lock +877 −6
…
2 unchanged lines
3 3
version = 4
4 4
5 5
[[package]]
6 +
name = "adler2"
7 +
version = "2.0.1"
8 +
source = "registry+https://github.com/rust-lang/crates.io-index"
9 +
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
10 +
11 +
[[package]]
6 12
name = "aho-corasick"
7 13
version = "1.1.5"
8 14
source = "registry+https://github.com/rust-lang/crates.io-index"
…
38 unchanged lines
47 53
source = "registry+https://github.com/rust-lang/crates.io-index"
48 54
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
49 55
dependencies = [
50 -
 "windows-sys",
56 +
 "windows-sys 0.61.2",
51 57
]
52 58
53 59
[[package]]
…
4 unchanged lines
58 64
dependencies = [
59 65
 "anstyle",
60 66
 "once_cell_polyfill",
61 -
 "windows-sys",
67 +
 "windows-sys 0.61.2",
62 68
]
63 69
64 70
[[package]]
…
9 unchanged lines
74 80
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
75 81
76 82
[[package]]
83 +
name = "atomic-waker"
84 +
version = "1.1.2"
85 +
source = "registry+https://github.com/rust-lang/crates.io-index"
86 +
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
87 +
88 +
[[package]]
77 89
name = "autocfg"
78 90
version = "1.5.1"
79 91
source = "registry+https://github.com/rust-lang/crates.io-index"
80 92
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
81 93
82 94
[[package]]
95 +
name = "axum"
96 +
version = "0.8.9"
97 +
source = "registry+https://github.com/rust-lang/crates.io-index"
98 +
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
99 +
dependencies = [
100 +
 "axum-core",
101 +
 "bytes",
102 +
 "form_urlencoded",
103 +
 "futures-util",
104 +
 "http",
105 +
 "http-body",
106 +
 "http-body-util",
107 +
 "hyper",
108 +
 "hyper-util",
109 +
 "itoa",
110 +
 "matchit",
111 +
 "memchr",
112 +
 "mime",
113 +
 "percent-encoding",
114 +
 "pin-project-lite",
115 +
 "serde_core",
116 +
 "serde_json",
117 +
 "serde_path_to_error",
118 +
 "serde_urlencoded",
119 +
 "sync_wrapper",
120 +
 "tokio",
121 +
 "tower",
122 +
 "tower-layer",
123 +
 "tower-service",
124 +
 "tracing",
125 +
]
126 +
127 +
[[package]]
128 +
name = "axum-core"
129 +
version = "0.5.6"
130 +
source = "registry+https://github.com/rust-lang/crates.io-index"
131 +
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
132 +
dependencies = [
133 +
 "bytes",
134 +
 "futures-core",
135 +
 "http",
136 +
 "http-body",
137 +
 "http-body-util",
138 +
 "mime",
139 +
 "pin-project-lite",
140 +
 "sync_wrapper",
141 +
 "tower-layer",
142 +
 "tower-service",
143 +
 "tracing",
144 +
]
145 +
146 +
[[package]]
147 +
name = "base64"
148 +
version = "0.22.1"
149 +
source = "registry+https://github.com/rust-lang/crates.io-index"
150 +
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
151 +
152 +
[[package]]
83 153
name = "base64ct"
84 154
version = "1.8.3"
85 155
source = "registry+https://github.com/rust-lang/crates.io-index"
…
38 unchanged lines
124 194
]
125 195
126 196
[[package]]
197 +
name = "bytes"
198 +
version = "1.12.1"
199 +
source = "registry+https://github.com/rust-lang/crates.io-index"
200 +
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
201 +
202 +
[[package]]
127 203
name = "cc"
128 204
version = "1.5.1"
129 205
source = "registry+https://github.com/rust-lang/crates.io-index"
…
83 unchanged lines
213 289
]
214 290
215 291
[[package]]
292 +
name = "crc32fast"
293 +
version = "1.5.2"
294 +
source = "registry+https://github.com/rust-lang/crates.io-index"
295 +
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
296 +
dependencies = [
297 +
 "cfg-if",
298 +
]
299 +
300 +
[[package]]
216 301
name = "crossbeam-deque"
217 302
version = "0.8.8"
218 303
source = "registry+https://github.com/rust-lang/crates.io-index"
…
86 unchanged lines
305 390
]
306 391
307 392
[[package]]
393 +
name = "displaydoc"
394 +
version = "0.2.7"
395 +
source = "registry+https://github.com/rust-lang/crates.io-index"
396 +
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
397 +
dependencies = [
398 +
 "proc-macro2",
399 +
 "quote",
400 +
 "syn 3.0.6",
401 +
]
402 +
403 +
[[package]]
308 404
name = "ed25519"
309 405
version = "3.0.0"
310 406
source = "registry+https://github.com/rust-lang/crates.io-index"
…
22 unchanged lines
333 429
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
334 430
dependencies = [
335 431
 "libc",
336 -
 "windows-sys",
432 +
 "windows-sys 0.61.2",
337 433
]
338 434
339 435
[[package]]
…
15 unchanged lines
355 451
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
356 452
357 453
[[package]]
454 +
name = "flate2"
455 +
version = "1.1.10"
456 +
source = "registry+https://github.com/rust-lang/crates.io-index"
457 +
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
458 +
dependencies = [
459 +
 "crc32fast",
460 +
 "miniz_oxide",
461 +
 "zlib-rs",
462 +
]
463 +
464 +
[[package]]
465 +
name = "form_urlencoded"
466 +
version = "1.2.2"
467 +
source = "registry+https://github.com/rust-lang/crates.io-index"
468 +
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
469 +
dependencies = [
470 +
 "percent-encoding",
471 +
]
472 +
473 +
[[package]]
474 +
name = "futures-channel"
475 +
version = "0.3.34"
476 +
source = "registry+https://github.com/rust-lang/crates.io-index"
477 +
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
478 +
dependencies = [
479 +
 "futures-core",
480 +
]
481 +
482 +
[[package]]
483 +
name = "futures-core"
484 +
version = "0.3.34"
485 +
source = "registry+https://github.com/rust-lang/crates.io-index"
486 +
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
487 +
488 +
[[package]]
489 +
name = "futures-task"
490 +
version = "0.3.34"
491 +
source = "registry+https://github.com/rust-lang/crates.io-index"
492 +
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
493 +
494 +
[[package]]
495 +
name = "futures-util"
496 +
version = "0.3.34"
497 +
source = "registry+https://github.com/rust-lang/crates.io-index"
498 +
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
499 +
dependencies = [
500 +
 "futures-core",
501 +
 "futures-task",
502 +
 "pin-project-lite",
503 +
 "slab",
504 +
]
505 +
506 +
[[package]]
358 507
name = "getrandom"
359 508
version = "0.2.17"
360 509
source = "registry+https://github.com/rust-lang/crates.io-index"
…
42 unchanged lines
403 552
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
404 553
405 554
[[package]]
555 +
name = "http"
556 +
version = "1.5.0"
557 +
source = "registry+https://github.com/rust-lang/crates.io-index"
558 +
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
559 +
dependencies = [
560 +
 "bytes",
561 +
 "itoa",
562 +
]
563 +
564 +
[[package]]
565 +
name = "http-body"
566 +
version = "1.1.0"
567 +
source = "registry+https://github.com/rust-lang/crates.io-index"
568 +
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
569 +
dependencies = [
570 +
 "bytes",
571 +
 "http",
572 +
]
573 +
574 +
[[package]]
575 +
name = "http-body-util"
576 +
version = "0.1.5"
577 +
source = "registry+https://github.com/rust-lang/crates.io-index"
578 +
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
579 +
dependencies = [
580 +
 "bytes",
581 +
 "futures-core",
582 +
 "http",
583 +
 "http-body",
584 +
 "pin-project-lite",
585 +
]
586 +
587 +
[[package]]
588 +
name = "httparse"
589 +
version = "1.10.1"
590 +
source = "registry+https://github.com/rust-lang/crates.io-index"
591 +
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
592 +
593 +
[[package]]
594 +
name = "httpdate"
595 +
version = "1.0.3"
596 +
source = "registry+https://github.com/rust-lang/crates.io-index"
597 +
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
598 +
599 +
[[package]]
406 600
name = "hybrid-array"
407 601
version = "0.4.15"
408 602
source = "registry+https://github.com/rust-lang/crates.io-index"
…
4 unchanged lines
413 607
]
414 608
415 609
[[package]]
610 +
name = "hyper"
611 +
version = "1.11.1"
612 +
source = "registry+https://github.com/rust-lang/crates.io-index"
613 +
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
614 +
dependencies = [
615 +
 "atomic-waker",
616 +
 "bytes",
617 +
 "futures-channel",
618 +
 "futures-core",
619 +
 "http",
620 +
 "http-body",
621 +
 "httparse",
622 +
 "httpdate",
623 +
 "itoa",
624 +
 "pin-project-lite",
625 +
 "smallvec",
626 +
 "tokio",
627 +
]
628 +
629 +
[[package]]
630 +
name = "hyper-util"
631 +
version = "0.1.21"
632 +
source = "registry+https://github.com/rust-lang/crates.io-index"
633 +
checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
634 +
dependencies = [
635 +
 "bytes",
636 +
 "http",
637 +
 "http-body",
638 +
 "hyper",
639 +
 "pin-project-lite",
640 +
 "tokio",
641 +
 "tower-service",
642 +
]
643 +
644 +
[[package]]
645 +
name = "icu_collections"
646 +
version = "2.3.0"
647 +
source = "registry+https://github.com/rust-lang/crates.io-index"
648 +
checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
649 +
dependencies = [
650 +
 "displaydoc",
651 +
 "potential_utf",
652 +
 "utf8_iter",
653 +
 "yoke",
654 +
 "zerofrom",
655 +
 "zerovec",
656 +
]
657 +
658 +
[[package]]
659 +
name = "icu_locale_core"
660 +
version = "2.3.0"
661 +
source = "registry+https://github.com/rust-lang/crates.io-index"
662 +
checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
663 +
dependencies = [
664 +
 "displaydoc",
665 +
 "litemap",
666 +
 "tinystr",
667 +
 "writeable",
668 +
 "zerovec",
669 +
]
670 +
671 +
[[package]]
672 +
name = "icu_normalizer"
673 +
version = "2.3.0"
674 +
source = "registry+https://github.com/rust-lang/crates.io-index"
675 +
checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
676 +
dependencies = [
677 +
 "icu_collections",
678 +
 "icu_normalizer_data",
679 +
 "icu_properties",
680 +
 "icu_provider",
681 +
 "smallvec",
682 +
 "zerovec",
683 +
]
684 +
685 +
[[package]]
686 +
name = "icu_normalizer_data"
687 +
version = "2.3.0"
688 +
source = "registry+https://github.com/rust-lang/crates.io-index"
689 +
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
690 +
691 +
[[package]]
692 +
name = "icu_properties"
693 +
version = "2.3.0"
694 +
source = "registry+https://github.com/rust-lang/crates.io-index"
695 +
checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
696 +
dependencies = [
697 +
 "displaydoc",
698 +
 "icu_collections",
699 +
 "icu_locale_core",
700 +
 "icu_properties_data",
701 +
 "icu_provider",
702 +
 "zerotrie",
703 +
 "zerovec",
704 +
]
705 +
706 +
[[package]]
707 +
name = "icu_properties_data"
708 +
version = "2.3.0"
709 +
source = "registry+https://github.com/rust-lang/crates.io-index"
710 +
checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
711 +
712 +
[[package]]
713 +
name = "icu_provider"
714 +
version = "2.3.1"
715 +
source = "registry+https://github.com/rust-lang/crates.io-index"
716 +
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
717 +
dependencies = [
718 +
 "displaydoc",
719 +
 "icu_locale_core",
720 +
 "writeable",
721 +
 "yoke",
722 +
 "zerofrom",
723 +
 "zerotrie",
724 +
 "zerovec",
725 +
]
726 +
727 +
[[package]]
728 +
name = "idna"
729 +
version = "1.1.0"
730 +
source = "registry+https://github.com/rust-lang/crates.io-index"
731 +
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
732 +
dependencies = [
733 +
 "idna_adapter",
734 +
 "smallvec",
735 +
 "utf8_iter",
736 +
]
737 +
738 +
[[package]]
739 +
name = "idna_adapter"
740 +
version = "1.2.2"
741 +
source = "registry+https://github.com/rust-lang/crates.io-index"
742 +
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
743 +
dependencies = [
744 +
 "icu_normalizer",
745 +
 "icu_properties",
746 +
]
747 +
748 +
[[package]]
416 749
name = "ignore"
417 750
version = "0.4.33"
418 751
source = "registry+https://github.com/rust-lang/crates.io-index"
…
16 unchanged lines
435 768
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
436 769
437 770
[[package]]
771 +
name = "itoa"
772 +
version = "1.0.18"
773 +
source = "registry+https://github.com/rust-lang/crates.io-index"
774 +
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
775 +
776 +
[[package]]
438 777
name = "keccak"
439 778
version = "0.2.2"
440 779
source = "registry+https://github.com/rust-lang/crates.io-index"
…
16 unchanged lines
457 796
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
458 797
459 798
[[package]]
799 +
name = "litemap"
800 +
version = "0.8.3"
801 +
source = "registry+https://github.com/rust-lang/crates.io-index"
802 +
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
803 +
804 +
[[package]]
460 805
name = "log"
461 806
version = "0.4.34"
462 807
source = "registry+https://github.com/rust-lang/crates.io-index"
463 808
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
464 809
465 810
[[package]]
811 +
name = "matchit"
812 +
version = "0.8.4"
813 +
source = "registry+https://github.com/rust-lang/crates.io-index"
814 +
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
815 +
816 +
[[package]]
466 817
name = "memchr"
467 818
version = "2.8.3"
468 819
source = "registry+https://github.com/rust-lang/crates.io-index"
469 820
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
470 821
471 822
[[package]]
823 +
name = "mime"
824 +
version = "0.3.17"
825 +
source = "registry+https://github.com/rust-lang/crates.io-index"
826 +
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
827 +
828 +
[[package]]
829 +
name = "miniz_oxide"
830 +
version = "0.9.1"
831 +
source = "registry+https://github.com/rust-lang/crates.io-index"
832 +
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
833 +
dependencies = [
834 +
 "adler2",
835 +
 "simd-adler32",
836 +
]
837 +
838 +
[[package]]
839 +
name = "mio"
840 +
version = "1.2.3"
841 +
source = "registry+https://github.com/rust-lang/crates.io-index"
842 +
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
843 +
dependencies = [
844 +
 "libc",
845 +
 "wasi",
846 +
 "windows-sys 0.61.2",
847 +
]
848 +
849 +
[[package]]
472 850
name = "ml-dsa"
473 851
version = "0.1.1"
474 852
source = "registry+https://github.com/rust-lang/crates.io-index"
…
49 unchanged lines
524 902
 "clap",
525 903
 "libc",
526 904
 "oxy-core",
905 +
 "oxy-server",
527 906
 "tempfile",
528 907
]
529 908
…
10 unchanged lines
540 919
 "similar",
541 920
 "tempfile",
542 921
 "thiserror",
922 +
 "ureq",
923 +
]
924 +
925 +
[[package]]
926 +
name = "oxy-server"
927 +
version = "0.1.0"
928 +
dependencies = [
929 +
 "axum",
930 +
 "oxy-core",
931 +
 "tempfile",
932 +
 "tokio",
543 933
]
544 934
545 935
[[package]]
936 +
name = "percent-encoding"
937 +
version = "2.3.2"
938 +
source = "registry+https://github.com/rust-lang/crates.io-index"
939 +
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
940 +
941 +
[[package]]
942 +
name = "pin-project-lite"
943 +
version = "0.2.17"
944 +
source = "registry+https://github.com/rust-lang/crates.io-index"
945 +
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
946 +
947 +
[[package]]
546 948
name = "pkcs8"
547 949
version = "0.11.0"
548 950
source = "registry+https://github.com/rust-lang/crates.io-index"
…
4 unchanged lines
553 955
]
554 956
555 957
[[package]]
958 +
name = "potential_utf"
959 +
version = "0.1.6"
960 +
source = "registry+https://github.com/rust-lang/crates.io-index"
961 +
checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
962 +
dependencies = [
963 +
 "zerovec",
964 +
]
965 +
966 +
[[package]]
556 967
name = "ppv-lite86"
557 968
version = "0.2.21"
558 969
source = "registry+https://github.com/rust-lang/crates.io-index"
…
80 unchanged lines
639 1050
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
640 1051
641 1052
[[package]]
1053 +
name = "ring"
1054 +
version = "0.17.14"
1055 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1056 +
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
1057 +
dependencies = [
1058 +
 "cc",
1059 +
 "cfg-if",
1060 +
 "getrandom 0.2.17",
1061 +
 "libc",
1062 +
 "untrusted",
1063 +
 "windows-sys 0.52.0",
1064 +
]
1065 +
1066 +
[[package]]
642 1067
name = "rustc_version"
643 1068
version = "0.4.1"
644 1069
source = "registry+https://github.com/rust-lang/crates.io-index"
…
12 unchanged lines
657 1082
 "errno",
658 1083
 "libc",
659 1084
 "linux-raw-sys",
660 -
 "windows-sys",
1085 +
 "windows-sys 0.61.2",
661 1086
]
662 1087
663 1088
[[package]]
1089 +
name = "rustls"
1090 +
version = "0.23.45"
1091 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1092 +
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
1093 +
dependencies = [
1094 +
 "log",
1095 +
 "once_cell",
1096 +
 "ring",
1097 +
 "rustls-pki-types",
1098 +
 "rustls-webpki",
1099 +
 "subtle",
1100 +
 "zeroize",
1101 +
]
1102 +
1103 +
[[package]]
1104 +
name = "rustls-pki-types"
1105 +
version = "1.15.1"
1106 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1107 +
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
1108 +
dependencies = [
1109 +
 "zeroize",
1110 +
]
1111 +
1112 +
[[package]]
1113 +
name = "rustls-webpki"
1114 +
version = "0.103.15"
1115 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1116 +
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
1117 +
dependencies = [
1118 +
 "ring",
1119 +
 "rustls-pki-types",
1120 +
 "untrusted",
1121 +
]
1122 +
1123 +
[[package]]
1124 +
name = "ryu"
1125 +
version = "1.0.23"
1126 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1127 +
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
1128 +
1129 +
[[package]]
664 1130
name = "same-file"
665 1131
version = "1.0.6"
666 1132
source = "registry+https://github.com/rust-lang/crates.io-index"
…
9 unchanged lines
676 1142
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
677 1143
678 1144
[[package]]
1145 +
name = "serde"
1146 +
version = "1.0.229"
1147 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1148 +
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
1149 +
dependencies = [
1150 +
 "serde_core",
1151 +
]
1152 +
1153 +
[[package]]
679 1154
name = "serde_core"
680 1155
version = "1.0.229"
681 1156
source = "registry+https://github.com/rust-lang/crates.io-index"
…
14 unchanged lines
696 1171
]
697 1172
698 1173
[[package]]
1174 +
name = "serde_json"
1175 +
version = "1.0.151"
1176 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1177 +
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
1178 +
dependencies = [
1179 +
 "itoa",
1180 +
 "memchr",
1181 +
 "serde",
1182 +
 "serde_core",
1183 +
 "zmij",
1184 +
]
1185 +
1186 +
[[package]]
1187 +
name = "serde_path_to_error"
1188 +
version = "0.1.20"
1189 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1190 +
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
1191 +
dependencies = [
1192 +
 "itoa",
1193 +
 "serde",
1194 +
 "serde_core",
1195 +
]
1196 +
1197 +
[[package]]
1198 +
name = "serde_urlencoded"
1199 +
version = "0.7.1"
1200 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1201 +
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
1202 +
dependencies = [
1203 +
 "form_urlencoded",
1204 +
 "itoa",
1205 +
 "ryu",
1206 +
 "serde",
1207 +
]
1208 +
1209 +
[[package]]
699 1210
name = "sha2"
700 1211
version = "0.11.0"
701 1212
source = "registry+https://github.com/rust-lang/crates.io-index"
…
32 unchanged lines
734 1245
]
735 1246
736 1247
[[package]]
1248 +
name = "simd-adler32"
1249 +
version = "0.3.10"
1250 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1251 +
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
1252 +
1253 +
[[package]]
737 1254
name = "similar"
738 1255
version = "2.7.0"
739 1256
source = "registry+https://github.com/rust-lang/crates.io-index"
740 1257
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
741 1258
742 1259
[[package]]
1260 +
name = "slab"
1261 +
version = "0.4.12"
1262 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1263 +
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
1264 +
1265 +
[[package]]
1266 +
name = "smallvec"
1267 +
version = "1.16.2"
1268 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1269 +
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
1270 +
1271 +
[[package]]
1272 +
name = "socket2"
1273 +
version = "0.6.5"
1274 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1275 +
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
1276 +
dependencies = [
1277 +
 "libc",
1278 +
 "windows-sys 0.61.2",
1279 +
]
1280 +
1281 +
[[package]]
743 1282
name = "spki"
744 1283
version = "0.8.0"
745 1284
source = "registry+https://github.com/rust-lang/crates.io-index"
…
10 unchanged lines
756 1295
checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620"
757 1296
758 1297
[[package]]
1298 +
name = "stable_deref_trait"
1299 +
version = "1.2.1"
1300 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1301 +
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
1302 +
1303 +
[[package]]
759 1304
name = "strsim"
760 1305
version = "0.11.1"
761 1306
source = "registry+https://github.com/rust-lang/crates.io-index"
…
28 unchanged lines
790 1335
]
791 1336
792 1337
[[package]]
1338 +
name = "sync_wrapper"
1339 +
version = "1.0.2"
1340 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1341 +
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
1342 +
1343 +
[[package]]
1344 +
name = "synstructure"
1345 +
version = "0.14.0"
1346 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1347 +
checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
1348 +
dependencies = [
1349 +
 "proc-macro2",
1350 +
 "quote",
1351 +
 "syn 3.0.6",
1352 +
]
1353 +
1354 +
[[package]]
793 1355
name = "tempfile"
794 1356
version = "3.27.0"
795 1357
source = "registry+https://github.com/rust-lang/crates.io-index"
…
3 unchanged lines
799 1361
 "getrandom 0.4.3",
800 1362
 "once_cell",
801 1363
 "rustix",
802 -
 "windows-sys",
1364 +
 "windows-sys 0.61.2",
803 1365
]
804 1366
805 1367
[[package]]
…
17 unchanged lines
823 1385
]
824 1386
825 1387
[[package]]
1388 +
name = "tinystr"
1389 +
version = "0.8.4"
1390 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1391 +
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
1392 +
dependencies = [
1393 +
 "displaydoc",
1394 +
 "zerovec",
1395 +
]
1396 +
1397 +
[[package]]
1398 +
name = "tokio"
1399 +
version = "1.53.1"
1400 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1401 +
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
1402 +
dependencies = [
1403 +
 "libc",
1404 +
 "mio",
1405 +
 "pin-project-lite",
1406 +
 "socket2",
1407 +
 "tokio-macros",
1408 +
 "windows-sys 0.61.2",
1409 +
]
1410 +
1411 +
[[package]]
1412 +
name = "tokio-macros"
1413 +
version = "2.7.2"
1414 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1415 +
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
1416 +
dependencies = [
1417 +
 "proc-macro2",
1418 +
 "quote",
1419 +
 "syn 3.0.6",
1420 +
]
1421 +
1422 +
[[package]]
1423 +
name = "tower"
1424 +
version = "0.5.3"
1425 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1426 +
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
1427 +
dependencies = [
1428 +
 "futures-core",
1429 +
 "futures-util",
1430 +
 "pin-project-lite",
1431 +
 "sync_wrapper",
1432 +
 "tokio",
1433 +
 "tower-layer",
1434 +
 "tower-service",
1435 +
 "tracing",
1436 +
]
1437 +
1438 +
[[package]]
1439 +
name = "tower-layer"
1440 +
version = "0.3.3"
1441 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1442 +
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
1443 +
1444 +
[[package]]
1445 +
name = "tower-service"
1446 +
version = "0.3.3"
1447 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1448 +
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
1449 +
1450 +
[[package]]
1451 +
name = "tracing"
1452 +
version = "0.1.44"
1453 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1454 +
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
1455 +
dependencies = [
1456 +
 "log",
1457 +
 "pin-project-lite",
1458 +
 "tracing-core",
1459 +
]
1460 +
1461 +
[[package]]
1462 +
name = "tracing-core"
1463 +
version = "0.1.36"
1464 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1465 +
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
1466 +
dependencies = [
1467 +
 "once_cell",
1468 +
]
1469 +
1470 +
[[package]]
826 1471
name = "typenum"
827 1472
version = "1.20.1"
828 1473
source = "registry+https://github.com/rust-lang/crates.io-index"
…
6 unchanged lines
835 1480
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
836 1481
837 1482
[[package]]
1483 +
name = "untrusted"
1484 +
version = "0.9.0"
1485 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1486 +
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
1487 +
1488 +
[[package]]
1489 +
name = "ureq"
1490 +
version = "2.12.1"
1491 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1492 +
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
1493 +
dependencies = [
1494 +
 "base64",
1495 +
 "flate2",
1496 +
 "log",
1497 +
 "once_cell",
1498 +
 "rustls",
1499 +
 "rustls-pki-types",
1500 +
 "url",
1501 +
 "webpki-roots 0.26.11",
1502 +
]
1503 +
1504 +
[[package]]
1505 +
name = "url"
1506 +
version = "2.5.8"
1507 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1508 +
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
1509 +
dependencies = [
1510 +
 "form_urlencoded",
1511 +
 "idna",
1512 +
 "percent-encoding",
1513 +
 "serde",
1514 +
]
1515 +
1516 +
[[package]]
1517 +
name = "utf8_iter"
1518 +
version = "1.0.4"
1519 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1520 +
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
1521 +
1522 +
[[package]]
838 1523
name = "utf8parse"
839 1524
version = "0.2.2"
840 1525
source = "registry+https://github.com/rust-lang/crates.io-index"
…
16 unchanged lines
857 1542
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
858 1543
859 1544
[[package]]
1545 +
name = "webpki-roots"
1546 +
version = "0.26.11"
1547 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1548 +
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
1549 +
dependencies = [
1550 +
 "webpki-roots 1.0.9",
1551 +
]
1552 +
1553 +
[[package]]
1554 +
name = "webpki-roots"
1555 +
version = "1.0.9"
1556 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1557 +
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
1558 +
dependencies = [
1559 +
 "rustls-pki-types",
1560 +
]
1561 +
1562 +
[[package]]
860 1563
name = "winapi-util"
861 1564
version = "0.1.11"
862 1565
source = "registry+https://github.com/rust-lang/crates.io-index"
863 1566
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
864 1567
dependencies = [
865 -
 "windows-sys",
1568 +
 "windows-sys 0.61.2",
866 1569
]
867 1570
868 1571
[[package]]
…
4 unchanged lines
873 1576
874 1577
[[package]]
875 1578
name = "windows-sys"
1579 +
version = "0.52.0"
1580 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1581 +
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
1582 +
dependencies = [
1583 +
 "windows-targets",
1584 +
]
1585 +
1586 +
[[package]]
1587 +
name = "windows-sys"
876 1588
version = "0.61.2"
877 1589
source = "registry+https://github.com/rust-lang/crates.io-index"
878 1590
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
…
2 unchanged lines
881 1593
]
882 1594
883 1595
[[package]]
1596 +
name = "windows-targets"
1597 +
version = "0.52.6"
1598 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1599 +
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
1600 +
dependencies = [
1601 +
 "windows_aarch64_gnullvm",
1602 +
 "windows_aarch64_msvc",
1603 +
 "windows_i686_gnu",
1604 +
 "windows_i686_gnullvm",
1605 +
 "windows_i686_msvc",
1606 +
 "windows_x86_64_gnu",
1607 +
 "windows_x86_64_gnullvm",
1608 +
 "windows_x86_64_msvc",
1609 +
]
1610 +
1611 +
[[package]]
1612 +
name = "windows_aarch64_gnullvm"
1613 +
version = "0.52.6"
1614 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1615 +
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
1616 +
1617 +
[[package]]
1618 +
name = "windows_aarch64_msvc"
1619 +
version = "0.52.6"
1620 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1621 +
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
1622 +
1623 +
[[package]]
1624 +
name = "windows_i686_gnu"
1625 +
version = "0.52.6"
1626 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1627 +
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
1628 +
1629 +
[[package]]
1630 +
name = "windows_i686_gnullvm"
1631 +
version = "0.52.6"
1632 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1633 +
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
1634 +
1635 +
[[package]]
1636 +
name = "windows_i686_msvc"
1637 +
version = "0.52.6"
1638 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1639 +
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
1640 +
1641 +
[[package]]
1642 +
name = "windows_x86_64_gnu"
1643 +
version = "0.52.6"
1644 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1645 +
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
1646 +
1647 +
[[package]]
1648 +
name = "windows_x86_64_gnullvm"
1649 +
version = "0.52.6"
1650 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1651 +
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
1652 +
1653 +
[[package]]
1654 +
name = "windows_x86_64_msvc"
1655 +
version = "0.52.6"
1656 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1657 +
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1658 +
1659 +
[[package]]
1660 +
name = "writeable"
1661 +
version = "0.6.4"
1662 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1663 +
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
1664 +
1665 +
[[package]]
1666 +
name = "yoke"
1667 +
version = "0.8.3"
1668 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1669 +
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
1670 +
dependencies = [
1671 +
 "stable_deref_trait",
1672 +
 "yoke-derive",
1673 +
 "zerofrom",
1674 +
]
1675 +
1676 +
[[package]]
1677 +
name = "yoke-derive"
1678 +
version = "0.8.3"
1679 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1680 +
checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
1681 +
dependencies = [
1682 +
 "proc-macro2",
1683 +
 "quote",
1684 +
 "syn 3.0.6",
1685 +
 "synstructure",
1686 +
]
1687 +
1688 +
[[package]]
884 1689
name = "zerocopy"
885 1690
version = "0.8.59"
886 1691
source = "registry+https://github.com/rust-lang/crates.io-index"
…
14 unchanged lines
901 1706
]
902 1707
903 1708
[[package]]
1709 +
name = "zerofrom"
1710 +
version = "0.1.8"
1711 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1712 +
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
1713 +
dependencies = [
1714 +
 "zerofrom-derive",
1715 +
]
1716 +
1717 +
[[package]]
1718 +
name = "zerofrom-derive"
1719 +
version = "0.1.8"
1720 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1721 +
checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
1722 +
dependencies = [
1723 +
 "proc-macro2",
1724 +
 "quote",
1725 +
 "syn 3.0.6",
1726 +
 "synstructure",
1727 +
]
1728 +
1729 +
[[package]]
904 1730
name = "zeroize"
905 1731
version = "1.9.0"
906 1732
source = "registry+https://github.com/rust-lang/crates.io-index"
907 1733
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
1734 +
1735 +
[[package]]
1736 +
name = "zerotrie"
1737 +
version = "0.2.5"
1738 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1739 +
checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
1740 +
dependencies = [
1741 +
 "displaydoc",
1742 +
 "yoke",
1743 +
 "zerofrom",
1744 +
]
1745 +
1746 +
[[package]]
1747 +
name = "zerovec"
1748 +
version = "0.11.8"
1749 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1750 +
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
1751 +
dependencies = [
1752 +
 "yoke",
1753 +
 "zerofrom",
1754 +
 "zerovec-derive",
1755 +
]
1756 +
1757 +
[[package]]
1758 +
name = "zerovec-derive"
1759 +
version = "0.11.6"
1760 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1761 +
checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
1762 +
dependencies = [
1763 +
 "proc-macro2",
1764 +
 "quote",
1765 +
 "syn 3.0.6",
1766 +
]
1767 +
1768 +
[[package]]
1769 +
name = "zlib-rs"
1770 +
version = "0.6.8"
1771 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1772 +
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
1773 +
1774 +
[[package]]
1775 +
name = "zmij"
1776 +
version = "1.0.23"
1777 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1778 +
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
modified Cargo.toml +2 −1
1 1
[workspace]
2 -
members = ["crates/oxy-core", "."]
2 +
members = ["crates/oxy-core", "crates/oxy-server", "."]
3 3
resolver = "2"
4 4
5 5
[workspace.package]
…
15 unchanged lines
21 21
22 22
[dependencies]
23 23
oxy-core = { path = "crates/oxy-core" }
24 +
oxy-server = { path = "crates/oxy-server" }
24 25
clap = { version = "4", features = ["derive"] }
25 26
anyhow = "1"
26 27
libc = "0.2"
…
3 unchanged lines
modified crates/oxy-core/Cargo.toml +1 −0
…
13 unchanged lines
14 14
ignore = "0.4"
15 15
ml-dsa = "0.1"
16 16
ed25519-dalek = "3"
17 +
ureq = "2"
17 18
18 19
[dev-dependencies]
19 20
tempfile = "3"
modified crates/oxy-core/src/protocol.rs +360 −6
1 1
//! Transport-agnostic protocol (spec §16): what a client asks of a remote and
2 2
//! what a server does with a push. `LocalRemote` serves another repository
3 -
//! directory in-process; an HTTPS transport implements the same trait and the
4 -
//! server side reuses `Repo::receive_push` and friends unchanged.
3 +
//! directory in-process, `HttpRemote` speaks to an `oxy-server` over HTTP(S),
4 +
//! and the server side reuses `Repo::receive_push` and friends unchanged.
5 +
//!
6 +
//! Wire messages are deterministic CBOR (`wire` module); over HTTP each is a
7 +
//! request or response body with content type `application/oxy-cbor`.
5 8
6 9
use crate::attest::Attest;
10 +
use crate::cbor::{self, Value};
7 11
use crate::hash::{ChangeHash, ObjectRef, ObjectType};
8 12
use crate::identity::Signature;
9 -
use crate::objects::Change;
13 +
use crate::objects::{self as o, Change, ObjectError};
10 14
use crate::repo::{Repo, RepoError, Result};
11 15
use std::path::Path;
12 16
17 +
pub const CONTENT_TYPE: &str = "application/oxy-cbor";
18 +
pub const PROTOCOL_VERSION: u64 = 1;
19 +
13 20
/// An attestation together with the signatures over its ObjectRef.
14 21
#[derive(Clone, Debug, PartialEq, Eq)]
15 22
pub struct SignedAttest {
…
53 unchanged lines
69 76
    fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>) -> Result<Vec<RefResult>>;
70 77
}
71 78
79 +
// ---------------------------------------------------------------- local
80 +
72 81
/// Another repository on this machine, served in-process.
73 82
pub struct LocalRemote {
74 83
    pub repo: Repo,
…
23 unchanged lines
98 107
    }
99 108
}
100 109
101 -
/// Open a remote by URL. Only local paths (optionally `file://`) exist so far.
102 -
pub fn open_remote(url: &str) -> Result<Box<dyn Remote>> {
110 +
// ---------------------------------------------------------------- http
111 +
112 +
/// A remote served by `oxy-server` at `<base>/oxy/v1/...`.
113 +
pub struct HttpRemote {
114 +
    base: String,
115 +
    token: Option<String>,
116 +
    agent: ureq::Agent,
117 +
}
118 +
119 +
impl HttpRemote {
120 +
    pub fn new(base: &str, token: Option<String>) -> HttpRemote {
121 +
        HttpRemote { base: base.trim_end_matches('/').to_string(), token, agent: ureq::Agent::new() }
122 +
    }
123 +
124 +
    fn call(&self, post: bool, endpoint: &str, body: Option<&[u8]>) -> Result<Vec<u8>> {
125 +
        let url = format!("{}/oxy/v1/{endpoint}", self.base);
126 +
        let mut req = if post { self.agent.post(&url) } else { self.agent.get(&url) };
127 +
        req = req.set("Accept", CONTENT_TYPE);
128 +
        if let Some(t) = &self.token {
129 +
            req = req.set("Authorization", &format!("Bearer {t}"));
130 +
        }
131 +
        let resp = match body {
132 +
            Some(b) => req.set("Content-Type", CONTENT_TYPE).send_bytes(b),
133 +
            None => req.call(),
134 +
        };
135 +
        match resp {
136 +
            Ok(r) => {
137 +
                let mut out = Vec::new();
138 +
                r.into_reader().read_to_end(&mut out).map_err(|e| RepoError::Msg(format!("{url}: {e}")))?;
139 +
                Ok(out)
140 +
            }
141 +
            Err(ureq::Error::Status(code, r)) => {
142 +
                let text = r.into_string().unwrap_or_default();
143 +
                Err(RepoError::Msg(format!("{url}: HTTP {code}: {}", text.trim())))
144 +
            }
145 +
            Err(e) => Err(RepoError::Msg(format!("{url}: {e}"))),
146 +
        }
147 +
    }
148 +
}
149 +
150 +
impl Remote for HttpRemote {
151 +
    fn repo_id(&self) -> Result<[u8; 16]> {
152 +
        wire::decode_capabilities(&self.call(false, "capabilities", None)?)
153 +
    }
154 +
    fn refs(&self) -> Result<Vec<RemoteRef>> {
155 +
        Ok(wire::decode_refs(&self.call(false, "refs", None)?)?.1)
156 +
    }
157 +
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
158 +
        wire::decode_hashes(&self.call(true, "negotiate", Some(&wire::encode_negotiate(have, want)))?)
159 +
    }
160 +
    fn fetch(&self, changes: &[ChangeHash]) -> Result<Bundle> {
161 +
        wire::decode_bundle(&self.call(true, "fetch", Some(&wire::encode_hashes(changes)))?)
162 +
    }
163 +
    fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>) -> Result<Vec<RefResult>> {
164 +
        wire::decode_push_response(&self.call(true, "push", Some(&wire::encode_push(&bundle, &updates)))?)
165 +
    }
166 +
}
167 +
168 +
/// Open a remote by URL: a local path (optionally `file://`), or `http(s)://`.
169 +
pub fn open_remote(url: &str, token: Option<String>) -> Result<Box<dyn Remote>> {
170 +
    if url.starts_with("http://") || url.starts_with("https://") {
171 +
        return Ok(Box::new(HttpRemote::new(url, token)));
172 +
    }
103 173
    let path = url.strip_prefix("file://").unwrap_or(url);
104 174
    if path.contains("://") {
105 -
        return Err(RepoError::Msg(format!("unsupported remote URL {url:?}: only local paths are implemented")));
175 +
        return Err(RepoError::Msg(format!("unsupported remote URL {url:?}")));
106 176
    }
107 177
    Ok(Box::new(LocalRemote::open(Path::new(path))?))
108 178
}
179 +
180 +
// ---------------------------------------------------------------- wire
181 +
182 +
/// CBOR encodings of the protocol messages (spec §16.1).
183 +
pub mod wire {
184 +
    use super::*;
185 +
186 +
    fn schema(msg: &str) -> RepoError {
187 +
        RepoError::Object(ObjectError::Schema(msg.into()))
188 +
    }
189 +
    fn sigs_value(sigs: &[Signature]) -> Value {
190 +
        Value::Array(sigs.iter().map(|s| s.to_value()).collect())
191 +
    }
192 +
    fn sigs_from(v: &Value) -> Result<Vec<Signature>> {
193 +
        Ok(o::array(v, "signatures")?.iter().map(Signature::from_value).collect::<std::result::Result<Vec<_>, _>>()?)
194 +
    }
195 +
    fn hashes_value(hs: &[ObjectRef]) -> Value {
196 +
        Value::Array(hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect())
197 +
    }
198 +
    fn hashes_from(v: &Value) -> Result<Vec<ObjectRef>> {
199 +
        Ok(o::array(v, "hashes")?.iter().map(|h| o::oref(h, "hash")).collect::<std::result::Result<Vec<_>, _>>()?)
200 +
    }
201 +
    fn attest_value(sa: &SignedAttest) -> Value {
202 +
        Value::Array(vec![Value::Bytes(sa.attest.encode()), sigs_value(&sa.sigs)])
203 +
    }
204 +
    fn attest_from(v: &Value) -> Result<SignedAttest> {
205 +
        let a = o::array(v, "signed attest")?;
206 +
        if a.len() != 2 {
207 +
            return Err(schema("signed attest: expected [attest, sigs]"));
208 +
        }
209 +
        Ok(SignedAttest { attest: Attest::decode(o::bytes(&a[0], "attest")?)?, sigs: sigs_from(&a[1])? })
210 +
    }
211 +
    fn opt_attest_value(sa: &Option<SignedAttest>) -> Value {
212 +
        sa.as_ref().map(attest_value).unwrap_or(Value::Null)
213 +
    }
214 +
    fn opt_attest_from(v: &Value) -> Result<Option<SignedAttest>> {
215 +
        if v.is_null() { Ok(None) } else { attest_from(v).map(Some) }
216 +
    }
217 +
    fn map<'a>(v: &'a Value, what: &str) -> Result<&'a o::Map> {
218 +
        Ok(o::map(v, what)?)
219 +
    }
220 +
    fn field<'a>(m: &'a o::Map, k: u64, what: &str) -> Result<&'a Value> {
221 +
        Ok(o::field(m, k, what)?)
222 +
    }
223 +
224 +
    pub fn bundle_value(b: &Bundle) -> Value {
225 +
        Value::map([
226 +
            (
227 +
                0,
228 +
                Value::Array(b.changes.iter().map(|(c, s)| Value::Array(vec![Value::Bytes(c.encode()), sigs_value(s)])).collect()),
229 +
            ),
230 +
            (1, Value::Array(b.attests.iter().map(attest_value).collect())),
231 +
            (
232 +
                2,
233 +
                Value::Array(
234 +
                    b.objects.iter().map(|(t, body)| Value::Array(vec![Value::Text(t.name().into()), Value::Bytes(body.clone())])).collect(),
235 +
                ),
236 +
            ),
237 +
        ])
238 +
    }
239 +
240 +
    pub fn bundle_from_value(v: &Value) -> Result<Bundle> {
241 +
        let m = map(v, "bundle")?;
242 +
        let mut b = Bundle::default();
243 +
        for c in o::array(field(m, 0, "changes")?, "changes")? {
244 +
            let pair = o::array(c, "change entry")?;
245 +
            if pair.len() != 2 {
246 +
                return Err(schema("change entry: expected [change, sigs]"));
247 +
            }
248 +
            b.changes.push((Change::decode(o::bytes(&pair[0], "change")?)?, sigs_from(&pair[1])?));
249 +
        }
250 +
        for a in o::array(field(m, 1, "attests")?, "attests")? {
251 +
            b.attests.push(attest_from(a)?);
252 +
        }
253 +
        for ob in o::array(field(m, 2, "objects")?, "objects")? {
254 +
            let pair = o::array(ob, "object")?;
255 +
            if pair.len() != 2 {
256 +
                return Err(schema("object: expected [type, body]"));
257 +
            }
258 +
            let ty = ObjectType::from_name(o::text(&pair[0], "object type")?).ok_or_else(|| schema("unknown object type"))?;
259 +
            b.objects.push((ty, o::bytes(&pair[1], "object body")?.to_vec()));
260 +
        }
261 +
        Ok(b)
262 +
    }
263 +
264 +
    pub fn encode_bundle(b: &Bundle) -> Vec<u8> {
265 +
        cbor::encode(&bundle_value(b))
266 +
    }
267 +
    pub fn decode_bundle(bytes: &[u8]) -> Result<Bundle> {
268 +
        bundle_from_value(&cbor::decode(bytes).map_err(ObjectError::from)?)
269 +
    }
270 +
271 +
    pub fn encode_capabilities(id: [u8; 16]) -> Vec<u8> {
272 +
        cbor::encode(&Value::map([
273 +
            (0, Value::Array(vec![Value::Uint(PROTOCOL_VERSION)])),
274 +
            (1, Value::Bytes(id.to_vec())),
275 +
            (2, Value::Array(vec![Value::Text("seal".into())])),
276 +
        ]))
277 +
    }
278 +
    pub fn decode_capabilities(bytes: &[u8]) -> Result<[u8; 16]> {
279 +
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
280 +
        let m = map(&v, "capabilities")?;
281 +
        let versions = o::array(field(m, 0, "versions")?, "versions")?;
282 +
        if !versions.iter().any(|x| x.as_uint() == Some(PROTOCOL_VERSION)) {
283 +
            return Err(RepoError::Msg("remote does not speak protocol version 1".into()));
284 +
        }
285 +
        o::bytes(field(m, 1, "repo id")?, "repo id")?.try_into().map_err(|_| schema("repo id: expected 16 bytes"))
286 +
    }
287 +
288 +
    pub fn encode_refs(id: [u8; 16], refs: &[RemoteRef]) -> Vec<u8> {
289 +
        cbor::encode(&Value::map([
290 +
            (
291 +
                0,
292 +
                Value::Array(
293 +
                    refs.iter()
294 +
                        .map(|r| {
295 +
                            Value::map([
296 +
                                (0, Value::Text(r.name.clone())),
297 +
                                (1, Value::Bytes(r.state.0.to_vec())),
298 +
                                (2, hashes_value(&r.heads)),
299 +
                                (3, opt_attest_value(&r.attestation)),
300 +
                                (4, r.last_serial.map(Value::Uint).unwrap_or(Value::Null)),
301 +
                            ])
302 +
                        })
303 +
                        .collect(),
304 +
                ),
305 +
            ),
306 +
            (1, Value::Bytes(id.to_vec())),
307 +
        ]))
308 +
    }
309 +
    pub fn decode_refs(bytes: &[u8]) -> Result<([u8; 16], Vec<RemoteRef>)> {
310 +
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
311 +
        let m = map(&v, "refs")?;
312 +
        let id: [u8; 16] = o::bytes(field(m, 1, "repo id")?, "repo id")?.try_into().map_err(|_| schema("repo id: expected 16 bytes"))?;
313 +
        let mut refs = Vec::new();
314 +
        for r in o::array(field(m, 0, "refs")?, "refs")? {
315 +
            let rm = map(r, "ref")?;
316 +
            let ls = field(rm, 4, "last serial")?;
317 +
            refs.push(RemoteRef {
318 +
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
319 +
                state: o::oref(field(rm, 1, "state")?, "state")?,
320 +
                heads: hashes_from(field(rm, 2, "heads")?)?,
321 +
                attestation: opt_attest_from(field(rm, 3, "attestation")?)?,
322 +
                last_serial: if ls.is_null() { None } else { Some(o::uint(ls, "last serial")?) },
323 +
            });
324 +
        }
325 +
        Ok((id, refs))
326 +
    }
327 +
328 +
    pub fn encode_negotiate(have: &[ChangeHash], want: &[ChangeHash]) -> Vec<u8> {
329 +
        cbor::encode(&Value::map([(0, hashes_value(have)), (1, hashes_value(want))]))
330 +
    }
331 +
    pub fn decode_negotiate(bytes: &[u8]) -> Result<(Vec<ChangeHash>, Vec<ChangeHash>)> {
332 +
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
333 +
        let m = map(&v, "negotiate")?;
334 +
        Ok((hashes_from(field(m, 0, "have")?)?, hashes_from(field(m, 1, "want")?)?))
335 +
    }
336 +
337 +
    pub fn encode_hashes(hs: &[ChangeHash]) -> Vec<u8> {
338 +
        cbor::encode(&Value::map([(0, hashes_value(hs))]))
339 +
    }
340 +
    pub fn decode_hashes(bytes: &[u8]) -> Result<Vec<ChangeHash>> {
341 +
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
342 +
        hashes_from(field(map(&v, "hashes")?, 0, "hashes")?)
343 +
    }
344 +
345 +
    pub fn encode_push(bundle: &Bundle, updates: &[RefUpdate]) -> Vec<u8> {
346 +
        cbor::encode(&Value::map([
347 +
            (0, bundle_value(bundle)),
348 +
            (
349 +
                1,
350 +
                Value::Array(
351 +
                    updates
352 +
                        .iter()
353 +
                        .map(|u| {
354 +
                            Value::map([
355 +
                                (0, Value::Text(u.name.clone())),
356 +
                                (1, Value::Bytes(u.old_state.0.to_vec())),
357 +
                                (2, Value::Bytes(u.new_state.0.to_vec())),
358 +
                                (3, Value::Bool(u.force)),
359 +
                                (4, opt_attest_value(&u.attestation)),
360 +
                            ])
361 +
                        })
362 +
                        .collect(),
363 +
                ),
364 +
            ),
365 +
        ]))
366 +
    }
367 +
    pub fn decode_push(bytes: &[u8]) -> Result<(Bundle, Vec<RefUpdate>)> {
368 +
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
369 +
        let m = map(&v, "push")?;
370 +
        let bundle = bundle_from_value(field(m, 0, "bundle")?)?;
371 +
        let mut updates = Vec::new();
372 +
        for u in o::array(field(m, 1, "updates")?, "updates")? {
373 +
            let um = map(u, "update")?;
374 +
            updates.push(RefUpdate {
375 +
                name: o::text(field(um, 0, "name")?, "name")?.to_owned(),
376 +
                old_state: o::oref(field(um, 1, "old_state")?, "old_state")?,
377 +
                new_state: o::oref(field(um, 2, "new_state")?, "new_state")?,
378 +
                force: field(um, 3, "force")?.as_bool().ok_or_else(|| schema("force: expected bool"))?,
379 +
                attestation: opt_attest_from(field(um, 4, "attestation")?)?,
380 +
            });
381 +
        }
382 +
        Ok((bundle, updates))
383 +
    }
384 +
385 +
    pub fn encode_push_response(results: &[RefResult]) -> Vec<u8> {
386 +
        cbor::encode(&Value::map([(
387 +
            0,
388 +
            Value::Array(
389 +
                results
390 +
                    .iter()
391 +
                    .map(|r| Value::map([(0, Value::Text(r.name.clone())), (1, Value::Bool(r.ok)), (2, Value::Text(r.reason.clone()))]))
392 +
                    .collect(),
393 +
            ),
394 +
        )]))
395 +
    }
396 +
    pub fn decode_push_response(bytes: &[u8]) -> Result<Vec<RefResult>> {
397 +
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
398 +
        let m = map(&v, "push response")?;
399 +
        let mut out = Vec::new();
400 +
        for r in o::array(field(m, 0, "results")?, "results")? {
401 +
            let rm = map(r, "result")?;
402 +
            out.push(RefResult {
403 +
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
404 +
                ok: field(rm, 1, "ok")?.as_bool().ok_or_else(|| schema("ok: expected bool"))?,
405 +
                reason: o::text(field(rm, 2, "reason")?, "reason")?.to_owned(),
406 +
            });
407 +
        }
408 +
        Ok(out)
409 +
    }
410 +
}
411 +
412 +
#[cfg(test)]
413 +
mod tests {
414 +
    use super::*;
415 +
    use crate::attest::AttestBody;
416 +
    use crate::objects::Hlc;
417 +
418 +
    #[test]
419 +
    fn wire_round_trips() {
420 +
        let c = crate::objects::tests::sample_change();
421 +
        let sk = crate::identity::SecretKey { ed_seed: [1; 32], ml_seed: [2; 32] };
422 +
        let id = sk.identity("a", "b", 0);
423 +
        let key = id.hash();
424 +
        let sig = sk.sign(crate::identity::Domain::Change, &key, &c.hash());
425 +
        let seal = Attest::seal([3; 16], vec![c.hash()]);
426 +
        let seal_sig = sk.sign(crate::identity::Domain::Attest, &key, &seal.hash());
427 +
        let bundle = Bundle {
428 +
            changes: vec![(c.clone(), vec![sig.clone()])],
429 +
            attests: vec![SignedAttest { attest: seal.clone(), sigs: vec![seal_sig] }],
430 +
            objects: vec![(ObjectType::Identity, id.encode()), (ObjectType::Chunk, b"xyz".to_vec())],
431 +
        };
432 +
        let d = wire::decode_bundle(&wire::encode_bundle(&bundle)).unwrap();
433 +
        assert_eq!(d.changes, bundle.changes);
434 +
        assert_eq!(d.attests, bundle.attests);
435 +
        assert_eq!(d.objects, bundle.objects);
436 +
437 +
        let att = Attest { repo: [3; 16], hlc: Hlc::default(), body: AttestBody::Ref { name: "main".into(), old_state: ObjectRef([1; 32]), new_state: ObjectRef([2; 32]), serial: 4 } };
438 +
        let refs = vec![
439 +
            RemoteRef { name: "main".into(), state: ObjectRef([2; 32]), heads: vec![c.hash()], attestation: Some(SignedAttest { attest: att.clone(), sigs: vec![sig.clone()] }), last_serial: Some(4) },
440 +
            RemoteRef { name: "dev".into(), state: ObjectRef([5; 32]), heads: vec![], attestation: None, last_serial: None },
441 +
        ];
442 +
        let (id_back, back) = wire::decode_refs(&wire::encode_refs([3; 16], &refs)).unwrap();
443 +
        assert_eq!(id_back, [3; 16]);
444 +
        assert_eq!(back.len(), 2);
445 +
        assert_eq!(back[0].attestation.as_ref().unwrap().attest, att);
446 +
        assert_eq!(back[1].last_serial, None);
447 +
448 +
        let (h, w) = wire::decode_negotiate(&wire::encode_negotiate(&[ObjectRef([1; 32])], &[ObjectRef([2; 32])])).unwrap();
449 +
        assert_eq!((h, w), (vec![ObjectRef([1; 32])], vec![ObjectRef([2; 32])]));
450 +
        assert_eq!(wire::decode_hashes(&wire::encode_hashes(&[c.hash()])).unwrap(), vec![c.hash()]);
451 +
452 +
        let up = RefUpdate { name: "main".into(), old_state: ObjectRef([1; 32]), new_state: ObjectRef([2; 32]), force: true, attestation: Some(SignedAttest { attest: att, sigs: vec![sig] }) };
453 +
        let (b2, ups) = wire::decode_push(&wire::encode_push(&bundle, std::slice::from_ref(&up))).unwrap();
454 +
        assert_eq!(b2.changes.len(), 1);
455 +
        assert_eq!(ups[0].name, "main");
456 +
        assert!(ups[0].force);
457 +
        assert_eq!(ups[0].attestation, up.attestation);
458 +
        let res = vec![RefResult { name: "main".into(), ok: false, reason: "stale".into() }];
459 +
        assert_eq!(wire::decode_push_response(&wire::encode_push_response(&res)).unwrap(), res);
460 +
        assert_eq!(wire::decode_capabilities(&wire::encode_capabilities([7; 16])).unwrap(), [7; 16]);
461 +
    }
462 +
}
modified crates/oxy-core/src/repo.rs +29 −7
…
60 unchanged lines
61 61
    pub bookmark: String,
62 62
    /// `remote.<name>.url`.
63 63
    pub remotes: BTreeMap<String, String>,
64 +
    /// `remote.<name>.token`: bearer token for HTTP pushes.
65 +
    pub remote_tokens: BTreeMap<String, String>,
64 66
}
65 67
66 68
impl Config {
…
6 unchanged lines
73 75
            sign_changes: false,
74 76
            bookmark: "main".into(),
75 77
            remotes: BTreeMap::new(),
78 +
            remote_tokens: BTreeMap::new(),
76 79
        }
77 80
    }
78 81
…
17 unchanged lines
96 99
                }
97 100
            }
98 101
            "workspace.bookmark" => self.bookmark = value.into(),
99 -
            k if k.starts_with("remote.") && k.ends_with(".url") => {
100 -
                let name = &k["remote.".len()..k.len() - ".url".len()];
102 +
            k if k.starts_with("remote.") && (k.ends_with(".url") || k.ends_with(".token")) => {
103 +
                let (name, is_url) = match k.strip_suffix(".url") {
104 +
                    Some(n) => (&n["remote.".len()..], true),
105 +
                    None => (&k["remote.".len()..k.len() - ".token".len()], false),
106 +
                };
101 107
                if name.is_empty() || name.contains('/') {
102 108
                    return fail(format!("invalid remote name in {key:?}"));
103 109
                }
110 +
                let map = if is_url { &mut self.remotes } else { &mut self.remote_tokens };
104 111
                if value.is_empty() {
105 -
                    self.remotes.remove(name);
112 +
                    map.remove(name);
113 +
                    if is_url {
114 +
                        self.remote_tokens.remove(name);
115 +
                    }
106 116
                } else {
107 -
                    self.remotes.insert(name.to_string(), value.into());
117 +
                    map.insert(name.to_string(), value.into());
108 118
                }
109 119
            }
110 120
            _ => return fail(format!("unknown configuration key {key:?}")),
…
26 unchanged lines
137 147
        );
138 148
        for (name, url) in &self.remotes {
139 149
            text.push_str(&format!("remote.{name}.url = \"{url}\"\n"));
150 +
            if let Some(t) = self.remote_tokens.get(name) {
151 +
                text.push_str(&format!("remote.{name}.token = \"{t}\"\n"));
152 +
            }
140 153
        }
141 154
        write_atomic(path, text.as_bytes())
142 155
    }
…
620 unchanged lines
763 776
        self.config.remotes.get(name).cloned().ok_or_else(|| RepoError::Msg(format!("no remote named {name:?}; add one with `oxy remote add`")))
764 777
    }
765 778
779 +
    /// Token for a remote: `remote.<name>.token`, else `$OXY_TOKEN`.
780 +
    fn remote_token(&self, name: &str) -> Option<String> {
781 +
        self.config.remote_tokens.get(name).cloned().or_else(|| std::env::var("OXY_TOKEN").ok())
782 +
    }
783 +
784 +
    fn open_named_remote(&self, name: &str) -> Result<Box<dyn crate::protocol::Remote>> {
785 +
        open_remote(&self.remote_url(name)?, self.remote_token(name))
786 +
    }
787 +
766 788
    fn serial_path(&self, remote: &str, bookmark: &str) -> PathBuf {
767 789
        self.oxy.join("remotes").join(remote).join(format!("{bookmark}.serial"))
768 790
    }
…
22 unchanged lines
791 813
792 814
    /// Fetch every bookmark of a remote into `remotes/<name>/bookmarks/*` (spec §10.2).
793 815
    pub fn fetch(&self, remote_name: &str) -> Result<Vec<(String, Vec<ChangeHash>, usize)>> {
794 -
        let remote = open_remote(&self.remote_url(remote_name)?)?;
816 +
        let remote = self.open_named_remote(remote_name)?;
795 817
        if remote.repo_id()? != self.id()? {
796 818
            return fail(format!("remote {remote_name:?} is a different repository (repository IDs differ)"));
797 819
        }
…
80 unchanged lines
878 900
    /// Push the current bookmark (spec §16.3), sealing this author's unsigned
879 901
    /// changes and attesting the reference when a key is available.
880 902
    pub fn push(&self, remote_name: &str, force: bool) -> Result<Vec<RefResult>> {
881 -
        let remote = open_remote(&self.remote_url(remote_name)?)?;
903 +
        let remote = self.open_named_remote(remote_name)?;
882 904
        if remote.repo_id()? != self.id()? {
883 905
            return fail(format!("remote {remote_name:?} is a different repository (repository IDs differ)"));
884 906
        }
…
74 unchanged lines
959 981
    /// Clone a remote into `dest`: same repository ID, `origin` remote,
960 982
    /// bookmark set to the remote's, working copy checked out.
961 983
    pub fn clone(url: &str, dest: &Path) -> Result<Repo> {
962 -
        let remote = open_remote(url)?;
984 +
        let remote = open_remote(url, std::env::var("OXY_TOKEN").ok())?;
963 985
        let id = remote.repo_id()?;
964 986
        let mut repo = Repo::init_with_id(dest, id)?;
965 987
        repo.config.remotes.insert("origin".into(), url.to_string());
…
1552 unchanged lines
modified src/main.rs +19 −1
…
58 unchanged lines
59 59
    },
60 60
    /// Report which conflicts of the working copy's base are resolved by the current files
61 61
    Resolve,
62 +
    /// Serve repositories under ROOT over HTTP (spec §16); TLS is terminated in front
63 +
    Serve {
64 +
        /// Directory with one repository per subdirectory
65 +
        #[arg(long, default_value = ".")]
66 +
        root: PathBuf,
67 +
        #[arg(long, default_value = "127.0.0.1:8890")]
68 +
        bind: String,
69 +
        /// Bearer token allowed to push (repeatable); none means open
70 +
        #[arg(long = "token")]
71 +
        tokens: Vec<String>,
72 +
    },
62 73
    /// Undo the last operation
63 74
    Undo,
64 75
    /// Manage the signing identity
…
3 unchanged lines
68 79
    },
69 80
    /// Verify the signatures on a change (default: every change in the log)
70 81
    Verify { change: Option<String> },
71 -
    /// Set a repository configuration key (author.name, author.email, author.key, core.sign, workspace.bookmark)
82 +
    /// Set a repository configuration key (author.*, core.sign, workspace.bookmark, remote.<name>.url|token)
72 83
    Config { key: String, value: String },
73 84
    /// List operations
74 85
    #[command(name = "op")]
…
317 unchanged lines
392 403
                Ok(2)
393 404
            }
394 405
        }
406 +
        Cmd::Serve { root, bind, tokens } => {
407 +
            let listener = std::net::TcpListener::bind(&bind).with_context(|| format!("binding {bind}"))?;
408 +
            let root = root.canonicalize()?;
409 +
            println!("Serving repositories under {} on http://{bind}/<repo>/oxy/v1/ ({})", root.display(), if tokens.is_empty() { "open: no push tokens configured" } else { "push requires a token" });
410 +
            oxy_server::run_blocking(listener, oxy_server::ServerConfig { root, tokens })?;
411 +
            Ok(0)
412 +
        }
395 413
        Cmd::Undo => {
396 414
            let repo = open()?;
397 415
            let op = repo.undo()?;
…
72 unchanged lines