Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Integrate radicle-artifact, update crates, and add UI for radicle-artifact
2color wants to merge 50 commits into main · opened 12 days ago

TODO:

  • Upgrade radicle-artifact once released
42 files changed +7853 -375 15955af7 1922e6e5
modified Cargo.lock
@@ -3,15 +3,6 @@
version = 4

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

-
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -23,7 +14,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
-
 "crypto-common",
+
 "crypto-common 0.1.6",
 "generic-array",
]

@@ -35,7 +26,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
 "cfg-if",
 "cipher",
-
 "cpufeatures",
+
 "cpufeatures 0.2.17",
]

[[package]]
@@ -227,9 +218,9 @@ dependencies = [

[[package]]
name = "anyhow"
-
version = "1.0.98"
+
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"

[[package]]
name = "arboard"
@@ -253,6 +244,21 @@ dependencies = [
]

[[package]]
+
name = "arc-swap"
+
version = "1.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
+
dependencies = [
+
 "rustversion",
+
]
+

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

+
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -265,6 +271,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"

[[package]]
+
name = "asn1-rs"
+
version = "0.7.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
+
dependencies = [
+
 "asn1-rs-derive",
+
 "asn1-rs-impl",
+
 "displaydoc",
+
 "nom",
+
 "num-traits",
+
 "rusticata-macros",
+
 "thiserror 2.0.18",
+
 "time",
+
]
+

+
[[package]]
+
name = "asn1-rs-derive"
+
version = "0.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.117",
+
 "synstructure",
+
]
+

+
[[package]]
+
name = "asn1-rs-impl"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
name = "ast_node"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -272,7 +317,29 @@ checksum = "2eb025ef00a6da925cf40870b9c8d008526b6004ece399cb0974209720f0b194"
dependencies = [
 "quote",
 "swc_macros_common",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
+
]
+

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

+
[[package]]
+
name = "async_io_stream"
+
version = "0.3.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c"
+
dependencies = [
+
 "futures",
+
 "pharos",
+
 "rustc_version",
]

[[package]]
@@ -299,6 +366,33 @@ dependencies = [
]

[[package]]
+
name = "atomic-polyfill"
+
version = "1.0.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
+
dependencies = [
+
 "critical-section",
+
]
+

+
[[package]]
+
name = "atomic-waker"
+
version = "1.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+

+
[[package]]
+
name = "attohttpc"
+
version = "0.30.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9"
+
dependencies = [
+
 "base64 0.22.1",
+
 "http",
+
 "log",
+
 "url",
+
]
+

+
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -357,18 +451,33 @@ dependencies = [
]

[[package]]
-
name = "backtrace"
-
version = "0.3.75"
+
name = "backon"
+
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+
checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef"
dependencies = [
-
 "addr2line",
-
 "cfg-if",
-
 "libc",
-
 "miniz_oxide",
-
 "object",
-
 "rustc-demangle",
-
 "windows-targets 0.52.6",
+
 "fastrand",
+
 "gloo-timers",
+
 "tokio",
+
]
+

+
[[package]]
+
name = "bao-tree"
+
version = "0.16.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "06384416b1825e6e04fde63262fda2dc408f5b64c02d04e0d8b70ae72c17a52b"
+
dependencies = [
+
 "blake3",
+
 "bytes",
+
 "futures-lite",
+
 "genawaiter",
+
 "iroh-io",
+
 "positioned-io",
+
 "range-collections",
+
 "self_cell",
+
 "serde",
+
 "smallvec",
+
 "tokio",
]

[[package]]
@@ -384,6 +493,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"

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

+
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -409,7 +524,7 @@ checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2"
dependencies = [
 "blowfish",
 "pbkdf2",
-
 "sha2",
+
 "sha2 0.10.9",
]

[[package]]
@@ -422,6 +537,12 @@ dependencies = [
]

[[package]]
+
name = "binary-merge"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "597bb81c80a54b6a4381b23faba8d7774b144c94cbd1d6fe3f1329bd776554ab"
+

+
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -444,11 +565,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"

[[package]]
name = "bitflags"
-
version = "2.9.0"
+
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
+
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
dependencies = [
-
 "serde",
+
 "serde_core",
]

[[package]]
@@ -464,6 +585,20 @@ dependencies = [
]

[[package]]
+
name = "blake3"
+
version = "1.8.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
+
dependencies = [
+
 "arrayref",
+
 "arrayvec",
+
 "cc",
+
 "cfg-if",
+
 "constant_time_eq",
+
 "cpufeatures 0.3.0",
+
]
+

+
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -473,6 +608,15 @@ dependencies = [
]

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

+
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -529,7 +673,7 @@ dependencies = [
 "proc-macro-crate 3.3.0",
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -615,9 +759,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"

[[package]]
name = "bytes"
-
version = "1.10.1"
+
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
dependencies = [
 "serde",
]
@@ -647,7 +791,7 @@ version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "cairo-sys-rs",
 "glib",
 "libc",
@@ -692,7 +836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b4a6cae9efc04cc6cbb8faf338d2c497c165c83e74509cf4dbedea948bbf6e5"
dependencies = [
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -715,7 +859,7 @@ dependencies = [
 "semver",
 "serde",
 "serde_json",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
]

[[package]]
@@ -795,7 +939,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
 "cfg-if",
 "cipher",
-
 "cpufeatures",
+
 "cpufeatures 0.2.17",
+
]
+

+
[[package]]
+
name = "chacha20"
+
version = "0.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
+
dependencies = [
+
 "cfg-if",
+
 "cpufeatures 0.3.0",
+
 "rand_core 0.10.1",
]

[[package]]
@@ -805,18 +960,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
 "iana-time-zone",
+
 "js-sys",
 "num-traits",
 "serde",
+
 "wasm-bindgen",
 "windows-link 0.2.1",
]

[[package]]
+
name = "cid"
+
version = "0.11.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "21a304f95f84d169a6f31c4d0a30d784643aaa0bbc9c1e449a2c23e963ec4971"
+
dependencies = [
+
 "multibase",
+
 "multihash",
+
 "serde",
+
 "serde_bytes",
+
 "unsigned-varint",
+
]
+

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

@@ -852,7 +1022,7 @@ dependencies = [
 "heck 0.5.0",
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -871,6 +1041,21 @@ dependencies = [
]

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

+
[[package]]
+
name = "cobs"
+
version = "0.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+
dependencies = [
+
 "thiserror 2.0.18",
+
]
+

+
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -887,18 +1072,52 @@ dependencies = [
]

[[package]]
+
name = "console"
+
version = "0.15.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
+
dependencies = [
+
 "encode_unicode",
+
 "libc",
+
 "once_cell",
+
 "unicode-width 0.2.2",
+
 "windows-sys 0.59.0",
+
]
+

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

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

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

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

[[package]]
+
name = "convert_case"
+
version = "0.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
+
dependencies = [
+
 "unicode-segmentation",
+
]
+

+
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -909,6 +1128,26 @@ dependencies = [
]

[[package]]
+
name = "cordyceps"
+
version = "0.3.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a"
+
dependencies = [
+
 "loom",
+
 "tracing",
+
]
+

+
[[package]]
+
name = "core-foundation"
+
version = "0.9.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+
dependencies = [
+
 "core-foundation-sys",
+
 "libc",
+
]
+

+
[[package]]
name = "core-foundation"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -930,8 +1169,8 @@ version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
-
 "bitflags 2.9.0",
-
 "core-foundation",
+
 "bitflags 2.11.1",
+
 "core-foundation 0.10.0",
 "core-graphics-types",
 "foreign-types",
 "libc",
@@ -943,8 +1182,8 @@ version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
dependencies = [
-
 "bitflags 2.9.0",
-
 "core-foundation",
+
 "bitflags 2.11.1",
+
 "core-foundation 0.10.0",
 "core-graphics-types",
 "foreign-types",
 "libc",
@@ -956,8 +1195,8 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
-
 "bitflags 2.9.0",
-
 "core-foundation",
+
 "bitflags 2.11.1",
+
 "core-foundation 0.10.0",
 "libc",
]

@@ -971,6 +1210,15 @@ dependencies = [
]

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

+
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -980,6 +1228,12 @@ dependencies = [
]

[[package]]
+
name = "critical-section"
+
version = "1.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
+

+
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -989,12 +1243,46 @@ dependencies = [
]

[[package]]
+
name = "crossbeam-epoch"
+
version = "0.9.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+
dependencies = [
+
 "crossbeam-utils",
+
]
+

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

[[package]]
+
name = "crossterm"
+
version = "0.25.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
+
dependencies = [
+
 "bitflags 1.3.2",
+
 "crossterm_winapi",
+
 "libc",
+
 "mio 0.8.11",
+
 "parking_lot",
+
 "signal-hook",
+
 "signal-hook-mio",
+
 "winapi",
+
]
+

+
[[package]]
+
name = "crossterm_winapi"
+
version = "0.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+
dependencies = [
+
 "winapi",
+
]
+

+
[[package]]
name = "crypto-bigint"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1018,6 +1306,15 @@ dependencies = [
]

[[package]]
+
name = "crypto-common"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
+
dependencies = [
+
 "hybrid-array",
+
]
+

+
[[package]]
name = "cssparser"
version = "0.29.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1054,7 +1351,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -1089,18 +1386,45 @@ dependencies = [
]

[[package]]
+
name = "ctutils"
+
version = "0.4.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
+
dependencies = [
+
 "cmov",
+
]
+

+
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
 "cfg-if",
-
 "cpufeatures",
+
 "cpufeatures 0.2.17",
+
 "curve25519-dalek-derive",
+
 "digest 0.10.7",
+
 "fiat-crypto 0.2.9",
+
 "rustc_version",
+
 "subtle",
+
]
+

+
[[package]]
+
name = "curve25519-dalek"
+
version = "5.0.0-pre.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "335f1947f241137a14106b6f5acc5918a5ede29c9d71d3f2cb1678d5075d9fc3"
+
dependencies = [
+
 "cfg-if",
+
 "cpufeatures 0.2.17",
 "curve25519-dalek-derive",
-
 "digest",
-
 "fiat-crypto",
+
 "digest 0.11.3",
+
 "fiat-crypto 0.3.0",
+
 "rand_core 0.10.1",
 "rustc_version",
+
 "serde",
 "subtle",
+
 "zeroize",
]

[[package]]
@@ -1111,7 +1435,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -1166,7 +1490,7 @@ dependencies = [
 "proc-macro2",
 "quote",
 "strsim",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -1177,20 +1501,20 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
 "darling_core",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
name = "data-encoding"
-
version = "2.9.0"
+
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
+
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"

[[package]]
name = "data-encoding-macro"
-
version = "0.1.18"
+
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d"
+
checksum = "3259c913752a86488b501ed8680446a5ed2d5aeac6e596cb23ba3800768ea32c"
dependencies = [
 "data-encoding",
 "data-encoding-macro-internal",
@@ -1198,12 +1522,12 @@ dependencies = [

[[package]]
name = "data-encoding-macro-internal"
-
version = "0.1.16"
+
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
+
checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
dependencies = [
 "data-encoding",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -1249,8 +1573,8 @@ dependencies = [
 "swc_ecma_parser",
 "swc_eq_ignore_macros",
 "text_lines",
-
 "thiserror 2.0.12",
-
 "unicode-width",
+
 "thiserror 2.0.18",
+
 "unicode-width 0.2.2",
 "url",
]

@@ -1272,7 +1596,7 @@ checksum = "9b565e60a9685cdf312c888665b5f8647ac692a7da7e058a5e2268a466da8eaf"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -1302,18 +1626,74 @@ version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
-
 "const-oid",
+
 "const-oid 0.9.6",
 "zeroize",
]

[[package]]
+
name = "der"
+
version = "0.8.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b"
+
dependencies = [
+
 "const-oid 0.10.2",
+
 "pem-rfc7468 1.0.0",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "der-parser"
+
version = "10.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
+
dependencies = [
+
 "asn1-rs",
+
 "displaydoc",
+
 "nom",
+
 "num-bigint",
+
 "num-traits",
+
 "rusticata-macros",
+
]
+

+
[[package]]
name = "deranged"
-
version = "0.4.0"
+
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
+
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
 "powerfmt",
-
 "serde",
+
 "serde_core",
+
]
+

+
[[package]]
+
name = "derive_builder"
+
version = "0.20.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
+
dependencies = [
+
 "derive_builder_macro",
+
]
+

+
[[package]]
+
name = "derive_builder_core"
+
version = "0.20.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
+
dependencies = [
+
 "darling",
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
+
name = "derive_builder_macro"
+
version = "0.20.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
+
dependencies = [
+
 "derive_builder_core",
+
 "syn 2.0.117",
]

[[package]]
@@ -1322,11 +1702,11 @@ version = "0.99.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
dependencies = [
-
 "convert_case",
+
 "convert_case 0.4.0",
 "proc-macro2",
 "quote",
 "rustc_version",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -1344,25 +1724,44 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
+
 "convert_case 0.10.0",
 "proc-macro2",
 "quote",
 "rustc_version",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
+
 "unicode-xid",
]

[[package]]
+
name = "diatomic-waker"
+
version = "0.2.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c"
+

+
[[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",
+
 "block-buffer 0.10.4",
+
 "const-oid 0.9.6",
+
 "crypto-common 0.1.6",
 "subtle",
]

[[package]]
+
name = "digest"
+
version = "0.11.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
+
dependencies = [
+
 "block-buffer 0.12.0",
+
 "const-oid 0.10.2",
+
 "crypto-common 0.2.1",
+
]
+

+
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1380,7 +1779,7 @@ dependencies = [
 "libc",
 "option-ext",
 "redox_users",
-
 "windows-sys 0.60.2",
+
 "windows-sys 0.61.2",
]

[[package]]
@@ -1389,7 +1788,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "block2 0.6.1",
 "libc",
 "objc2 0.6.4",
@@ -1403,7 +1802,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -1426,7 +1825,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -1471,7 +1870,7 @@ dependencies = [
 "indexmap 2.9.0",
 "rustc-hash",
 "serde",
-
 "unicode-width",
+
 "unicode-width 0.2.2",
]

[[package]]
@@ -1577,12 +1976,12 @@ version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
-
 "der",
-
 "digest",
+
 "der 0.7.10",
+
 "digest 0.10.7",
 "elliptic-curve",
 "rfc6979",
 "signature 2.2.0",
-
 "spki",
+
 "spki 0.7.3",
]

[[package]]
@@ -1604,15 +2003,42 @@ dependencies = [
]

[[package]]
+
name = "ed25519"
+
version = "3.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a"
+
dependencies = [
+
 "pkcs8 0.11.0",
+
 "serdect",
+
 "signature 3.0.0",
+
]
+

+
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
-
 "curve25519-dalek",
+
 "curve25519-dalek 4.1.3",
 "ed25519 2.2.3",
-
 "sha2",
+
 "sha2 0.10.9",
+
 "subtle",
+
]
+

+
[[package]]
+
name = "ed25519-dalek"
+
version = "3.0.0-pre.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "20449acd54b660981ae5caa2bcb56d1fe7f25f2e37a38ec507400fab034d4bb6"
+
dependencies = [
+
 "curve25519-dalek 5.0.0-pre.6",
+
 "ed25519 3.0.0",
+
 "rand_core 0.10.1",
+
 "serde",
+
 "sha2 0.11.0",
+
 "signature 3.0.0",
 "subtle",
+
 "zeroize",
]

[[package]]
@@ -1627,13 +2053,13 @@ version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
-
 "base16ct",
+
 "base16ct 0.2.0",
 "crypto-bigint",
-
 "digest",
+
 "digest 0.10.7",
 "ff",
 "generic-array",
 "group",
-
 "pkcs8",
+
 "pkcs8 0.10.2",
 "rand_core 0.6.4",
 "sec1",
 "subtle",
@@ -1661,6 +2087,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"

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

+
[[package]]
+
name = "embedded-io"
+
version = "0.6.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+

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

+
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1670,6 +2114,17 @@ dependencies = [
]

[[package]]
+
name = "enum-assoc"
+
version = "1.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1718,6 +2173,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3afcf4effa2c44390b9912544582d5af29e10dc4c816c5dbebf748e1c7416faa"

[[package]]
+
name = "fastbloom"
+
version = "0.17.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8"
+
dependencies = [
+
 "foldhash 0.2.0",
+
 "libm",
+
 "portable-atomic",
+
 "siphasher 1.0.1",
+
]
+

+
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1758,6 +2225,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"

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

+
[[package]]
name = "field-offset"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1831,7 +2304,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -1856,7 +2329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ff35a391aef949120a0340d690269b3d9f63460a6106e99bd07b961f345ea9"
dependencies = [
 "swc_macros_common",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -1876,12 +2349,41 @@ dependencies = [
]

[[package]]
+
name = "futures"
+
version = "0.3.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+
dependencies = [
+
 "futures-channel",
+
 "futures-core",
+
 "futures-executor",
+
 "futures-io",
+
 "futures-sink",
+
 "futures-task",
+
 "futures-util",
+
]
+

+
[[package]]
+
name = "futures-buffered"
+
version = "0.2.13"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5"
+
dependencies = [
+
 "cordyceps",
+
 "diatomic-waker",
+
 "futures-core",
+
 "pin-project-lite",
+
 "spin 0.10.0",
+
]
+

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

[[package]]
@@ -1908,6 +2410,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"

[[package]]
+
name = "futures-lite"
+
version = "2.6.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+
dependencies = [
+
 "fastrand",
+
 "futures-core",
+
 "futures-io",
+
 "parking",
+
 "pin-project-lite",
+
]
+

+
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1915,7 +2430,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -1936,6 +2451,7 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
+
 "futures-channel",
 "futures-core",
 "futures-io",
 "futures-macro",
@@ -1948,6 +2464,15 @@ dependencies = [
]

[[package]]
+
name = "fuzzy-matcher"
+
version = "0.3.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
+
dependencies = [
+
 "thread_local",
+
]
+

+
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2056,6 +2581,52 @@ dependencies = [
]

[[package]]
+
name = "genawaiter"
+
version = "0.99.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0"
+
dependencies = [
+
 "futures-core",
+
 "genawaiter-macro",
+
 "genawaiter-proc-macro",
+
 "proc-macro-hack",
+
]
+

+
[[package]]
+
name = "genawaiter-macro"
+
version = "0.99.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc"
+

+
[[package]]
+
name = "genawaiter-proc-macro"
+
version = "0.99.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738"
+
dependencies = [
+
 "proc-macro-error 0.4.12",
+
 "proc-macro-hack",
+
 "proc-macro2",
+
 "quote",
+
 "syn 1.0.109",
+
]
+

+
[[package]]
+
name = "generator"
+
version = "0.8.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9"
+
dependencies = [
+
 "cc",
+
 "cfg-if",
+
 "libc",
+
 "log",
+
 "rustversion",
+
 "windows-link 0.2.1",
+
 "windows-result 0.4.1",
+
]
+

+
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2094,8 +2665,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
 "cfg-if",
+
 "js-sys",
 "libc",
 "wasi 0.11.0+wasi-snapshot-preview1",
+
 "wasm-bindgen",
]

[[package]]
@@ -2106,11 +2679,27 @@ checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
 "cfg-if",
 "libc",
-
 "r-efi",
+
 "r-efi 5.2.0",
 "wasi 0.14.2+wasi-0.2.4",
]

[[package]]
+
name = "getrandom"
+
version = "0.4.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+
dependencies = [
+
 "cfg-if",
+
 "js-sys",
+
 "libc",
+
 "r-efi 6.0.0",
+
 "rand_core 0.10.1",
+
 "wasip2",
+
 "wasip3",
+
 "wasm-bindgen",
+
]
+

+
[[package]]
name = "ghash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2121,12 +2710,6 @@ dependencies = [
]

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

-
[[package]]
name = "gio"
version = "0.18.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2187,7 +2770,7 @@ dependencies = [
 "git-ref-format-core",
 "proc-macro-error2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -2196,7 +2779,7 @@ version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "libc",
 "libgit2-sys",
 "log",
@@ -2209,7 +2792,7 @@ version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "futures-channel",
 "futures-core",
 "futures-executor",
@@ -2234,10 +2817,10 @@ checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
dependencies = [
 "heck 0.4.1",
 "proc-macro-crate 2.0.0",
-
 "proc-macro-error",
+
 "proc-macro-error 1.0.4",
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -2257,6 +2840,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"

[[package]]
+
name = "gloo-timers"
+
version = "0.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
+
dependencies = [
+
 "futures-channel",
+
 "futures-core",
+
 "js-sys",
+
 "wasm-bindgen",
+
]
+

+
[[package]]
name = "gobject-sys"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2324,10 +2919,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d"
dependencies = [
 "proc-macro-crate 1.3.1",
-
 "proc-macro-error",
+
 "proc-macro-error 1.0.4",
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
+
name = "h2"
+
version = "0.4.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
+
dependencies = [
+
 "atomic-waker",
+
 "bytes",
+
 "fnv",
+
 "futures-core",
+
 "futures-sink",
+
 "http",
+
 "indexmap 2.9.0",
+
 "slab",
+
 "tokio",
+
 "tokio-util",
+
 "tracing",
+
]
+

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

[[package]]
@@ -2361,6 +2984,31 @@ dependencies = [
]

[[package]]
+
name = "hashbrown"
+
version = "0.17.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
dependencies = [
+
 "allocator-api2",
+
 "equivalent",
+
 "foldhash 0.2.0",
+
]
+

+
[[package]]
+
name = "heapless"
+
version = "0.7.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
+
dependencies = [
+
 "atomic-polyfill",
+
 "hash32",
+
 "rustc_version",
+
 "serde",
+
 "spin 0.9.8",
+
 "stable_deref_trait",
+
]
+

+
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2379,22 +3027,99 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"

[[package]]
-
name = "hmac"
-
version = "0.12.1"
+
name = "hickory-net"
+
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+
checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"
dependencies = [
-
 "digest",
+
 "async-trait",
+
 "bytes",
+
 "cfg-if",
+
 "data-encoding",
+
 "futures-channel",
+
 "futures-io",
+
 "futures-util",
+
 "h2",
+
 "hickory-proto",
+
 "http",
+
 "idna",
+
 "ipnet",
+
 "jni 0.22.4",
+
 "rand 0.10.1",
+
 "rustls",
+
 "thiserror 2.0.18",
+
 "tinyvec",
+
 "tokio",
+
 "tokio-rustls",
+
 "tracing",
+
 "url",
]

[[package]]
-
name = "hstr"
-
version = "3.0.4"
+
name = "hickory-proto"
+
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "faa57007c3c9dab34df2fa4c1fb52fe9c34ec5a27ed9d8edea53254b50cd7887"
+
checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643"
dependencies = [
-
 "hashbrown 0.14.5",
-
 "new_debug_unreachable",
+
 "data-encoding",
+
 "idna",
+
 "ipnet",
+
 "jni 0.22.4",
+
 "once_cell",
+
 "prefix-trie",
+
 "rand 0.10.1",
+
 "ring",
+
 "thiserror 2.0.18",
+
 "tinyvec",
+
 "tracing",
+
 "url",
+
]
+

+
[[package]]
+
name = "hickory-resolver"
+
version = "0.26.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"
+
dependencies = [
+
 "cfg-if",
+
 "futures-util",
+
 "hickory-net",
+
 "hickory-proto",
+
 "ipconfig",
+
 "ipnet",
+
 "jni 0.22.4",
+
 "moka",
+
 "ndk-context",
+
 "once_cell",
+
 "parking_lot",
+
 "rand 0.10.1",
+
 "resolv-conf",
+
 "rustls",
+
 "smallvec",
+
 "system-configuration",
+
 "thiserror 2.0.18",
+
 "tokio",
+
 "tokio-rustls",
+
 "tracing",
+
]
+

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

+
[[package]]
+
name = "hstr"
+
version = "3.0.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "faa57007c3c9dab34df2fa4c1fb52fe9c34ec5a27ed9d8edea53254b50cd7887"
+
dependencies = [
+
 "hashbrown 0.14.5",
+
 "new_debug_unreachable",
 "once_cell",
 "rustc-hash",
 "serde",
@@ -2470,6 +3195,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"

[[package]]
+
name = "hybrid-array"
+
version = "0.4.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5"
+
dependencies = [
+
 "typenum",
+
]
+

+
[[package]]
name = "hyper"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2478,6 +3212,7 @@ dependencies = [
 "bytes",
 "futures-channel",
 "futures-util",
+
 "h2",
 "http",
 "http-body",
 "httparse",
@@ -2490,6 +3225,21 @@ dependencies = [
]

[[package]]
+
name = "hyper-rustls"
+
version = "0.27.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+
dependencies = [
+
 "http",
+
 "hyper",
+
 "hyper-util",
+
 "rustls",
+
 "tokio",
+
 "tokio-rustls",
+
 "tower-service",
+
]
+

+
[[package]]
name = "hyper-util"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2634,12 +3384,24 @@ dependencies = [
]

[[package]]
+
name = "id-arena"
+
version = "2.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+

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

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

+
[[package]]
name = "idna"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2661,6 +3423,26 @@ dependencies = [
]

[[package]]
+
name = "igd-next"
+
version = "0.17.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bac9a3c8278f43b4cd8463380f4a25653ac843e5b177e1d3eaf849cc9ba10d4d"
+
dependencies = [
+
 "attohttpc",
+
 "bytes",
+
 "futures",
+
 "http",
+
 "http-body-util",
+
 "hyper",
+
 "hyper-util",
+
 "log",
+
 "rand 0.10.1",
+
 "tokio",
+
 "url",
+
 "xmltree",
+
]
+

+
[[package]]
name = "image"
version = "0.25.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2696,6 +3478,19 @@ dependencies = [
]

[[package]]
+
name = "indicatif"
+
version = "0.17.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
+
dependencies = [
+
 "console",
+
 "number_prefix",
+
 "portable-atomic",
+
 "unicode-width 0.2.2",
+
 "web-time",
+
]
+

+
[[package]]
name = "infer"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2715,10 +3510,52 @@ dependencies = [
]

[[package]]
+
name = "inplace-vec-builder"
+
version = "0.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cf64c2edc8226891a71f127587a2861b132d2b942310843814d5001d99a1d307"
+
dependencies = [
+
 "smallvec",
+
]
+

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

+
[[package]]
+
name = "ipconfig"
+
version = "0.3.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
+
dependencies = [
+
 "socket2",
+
 "widestring",
+
 "windows-registry",
+
 "windows-result 0.4.1",
+
 "windows-sys 0.61.2",
+
]
+

+
[[package]]
name = "ipnet"
-
version = "2.11.0"
+
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
dependencies = [
+
 "serde",
+
]

[[package]]
name = "iri-string"
@@ -2731,6 +3568,293 @@ dependencies = [
]

[[package]]
+
name = "iroh"
+
version = "1.0.0-rc.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b98e206e3d3f2642f5c08c413755fc0ac19b54ae1a656af88be03454ce3ed2e6"
+
dependencies = [
+
 "backon",
+
 "blake3",
+
 "bytes",
+
 "cfg_aliases",
+
 "ctutils",
+
 "data-encoding",
+
 "derive_more 2.1.1",
+
 "ed25519-dalek 3.0.0-pre.7",
+
 "futures-util",
+
 "getrandom 0.4.2",
+
 "hickory-resolver",
+
 "http",
+
 "ipnet",
+
 "iroh-base",
+
 "iroh-dns",
+
 "iroh-metrics",
+
 "iroh-relay",
+
 "n0-error",
+
 "n0-future",
+
 "n0-watcher",
+
 "netwatch",
+
 "noq",
+
 "noq-proto",
+
 "noq-udp",
+
 "papaya",
+
 "pin-project",
+
 "portable-atomic",
+
 "portmapper",
+
 "rand 0.10.1",
+
 "reqwest",
+
 "rustc-hash",
+
 "rustls",
+
 "rustls-pki-types",
+
 "rustls-webpki",
+
 "serde",
+
 "smallvec",
+
 "strum",
+
 "time",
+
 "tokio",
+
 "tokio-stream",
+
 "tokio-util",
+
 "tracing",
+
 "url",
+
 "wasm-bindgen-futures",
+
 "webpki-roots",
+
]
+

+
[[package]]
+
name = "iroh-base"
+
version = "1.0.0-rc.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2160a45265eba3bd290ce698f584c9b088bee47e518e9ec4460d5e5888ef660e"
+
dependencies = [
+
 "curve25519-dalek 5.0.0-pre.6",
+
 "data-encoding",
+
 "data-encoding-macro",
+
 "derive_more 2.1.1",
+
 "digest 0.11.3",
+
 "ed25519-dalek 3.0.0-pre.7",
+
 "getrandom 0.4.2",
+
 "n0-error",
+
 "rand 0.10.1",
+
 "serde",
+
 "sha2 0.11.0",
+
 "url",
+
 "zeroize",
+
 "zeroize_derive",
+
]
+

+
[[package]]
+
name = "iroh-blobs"
+
version = "0.101.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c6e351f5c1db08dcfb456e6299bda0881e1ba95a360f0913a5e57344c1538fb2"
+
dependencies = [
+
 "arrayvec",
+
 "bao-tree",
+
 "bytes",
+
 "cfg_aliases",
+
 "chrono",
+
 "constant_time_eq",
+
 "data-encoding",
+
 "derive_more 2.1.1",
+
 "genawaiter",
+
 "getrandom 0.4.2",
+
 "hex",
+
 "iroh",
+
 "iroh-base",
+
 "iroh-io",
+
 "iroh-metrics",
+
 "iroh-tickets",
+
 "iroh-util",
+
 "irpc",
+
 "n0-error",
+
 "n0-future",
+
 "nested_enum_utils",
+
 "noq",
+
 "postcard",
+
 "rand 0.10.1",
+
 "range-collections",
+
 "redb",
+
 "ref-cast",
+
 "reflink-copy",
+
 "self_cell",
+
 "serde",
+
 "smallvec",
+
 "tokio",
+
 "tracing",
+
]
+

+
[[package]]
+
name = "iroh-dns"
+
version = "1.0.0-rc.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c8b6d2946350d398c9d2d795bb99b04f22e8414c8a8ad9c5c3c0c5b7899af9a4"
+
dependencies = [
+
 "arc-swap",
+
 "cfg_aliases",
+
 "derive_more 2.1.1",
+
 "hickory-resolver",
+
 "iroh-base",
+
 "n0-error",
+
 "n0-future",
+
 "ndk-context",
+
 "rand 0.10.1",
+
 "reqwest",
+
 "rustls",
+
 "simple-dns",
+
 "strum",
+
 "tokio",
+
 "tracing",
+
 "url",
+
]
+

+
[[package]]
+
name = "iroh-io"
+
version = "0.6.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e0a5feb781017b983ff1b155cd1faf8174da2acafd807aa482876da2d7e6577a"
+
dependencies = [
+
 "bytes",
+
 "futures-lite",
+
 "pin-project",
+
 "smallvec",
+
 "tokio",
+
]
+

+
[[package]]
+
name = "iroh-metrics"
+
version = "1.0.0-rc.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d102597d0ee523f17fdb672c532395e634dbe945429284c811430d63bacc0d8a"
+
dependencies = [
+
 "iroh-metrics-derive",
+
 "itoa",
+
 "n0-error",
+
 "portable-atomic",
+
 "ryu",
+
 "serde",
+
 "tracing",
+
]
+

+
[[package]]
+
name = "iroh-metrics-derive"
+
version = "1.0.0-rc.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "91c8e0c97f1dc787107f388433c349397c565572fe6406d600ff7bb7b7fe3b30"
+
dependencies = [
+
 "heck 0.5.0",
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
+
name = "iroh-relay"
+
version = "1.0.0-rc.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "54f490405e42dd2ecf16be18a3587d2665401e94a498094f12322eaa6d5ebb2b"
+
dependencies = [
+
 "blake3",
+
 "bytes",
+
 "cfg_aliases",
+
 "data-encoding",
+
 "derive_more 2.1.1",
+
 "getrandom 0.4.2",
+
 "hickory-resolver",
+
 "http",
+
 "http-body-util",
+
 "hyper",
+
 "hyper-util",
+
 "iroh-base",
+
 "iroh-dns",
+
 "iroh-metrics",
+
 "lru",
+
 "n0-error",
+
 "n0-future",
+
 "noq",
+
 "noq-proto",
+
 "num_enum",
+
 "pin-project",
+
 "postcard",
+
 "rand 0.10.1",
+
 "reqwest",
+
 "rustls",
+
 "rustls-pki-types",
+
 "serde",
+
 "serde_bytes",
+
 "strum",
+
 "tokio",
+
 "tokio-rustls",
+
 "tokio-util",
+
 "tokio-websockets",
+
 "tracing",
+
 "url",
+
 "vergen-gitcl",
+
 "webpki-roots",
+
 "ws_stream_wasm",
+
]
+

+
[[package]]
+
name = "iroh-tickets"
+
version = "1.0.0-rc.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0a4b7fbfa10582f6b4f6b013eef1d21987d3df5fd42c0f7707d5de6abd34f8e9"
+
dependencies = [
+
 "data-encoding",
+
 "derive_more 2.1.1",
+
 "iroh-base",
+
 "n0-error",
+
 "postcard",
+
 "serde",
+
]
+

+
[[package]]
+
name = "iroh-util"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e7705450a124b2b05f7caad505620ab5ac3bf4eb6b85018e6b9bca36329fd031"
+
dependencies = [
+
 "derive_more 2.1.1",
+
 "iroh",
+
 "n0-error",
+
 "n0-future",
+
 "tokio",
+
 "tracing",
+
]
+

+
[[package]]
+
name = "irpc"
+
version = "0.15.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0d38567eed2ed120e1040386930eb3b9ce6ca8a94b13c20a1b3b6535f253b00c"
+
dependencies = [
+
 "futures-buffered",
+
 "futures-util",
+
 "irpc-derive",
+
 "n0-error",
+
 "n0-future",
+
 "noq",
+
 "postcard",
+
 "rcgen",
+
 "rustls",
+
 "serde",
+
 "smallvec",
+
 "tokio",
+
 "tokio-util",
+
 "tracing",
+
]
+

+
[[package]]
+
name = "irpc-derive"
+
version = "0.15.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6d8030c02dce4c9a8aecfb6e0870ee13ba3060096d88f6c1309919af8f197793"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2748,7 +3872,7 @@ dependencies = [
 "heck 0.5.0",
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -2805,7 +3929,7 @@ dependencies = [
 "cesu8",
 "cfg-if",
 "combine",
-
 "jni-sys",
+
 "jni-sys 0.3.0",
 "log",
 "thiserror 1.0.69",
 "walkdir",
@@ -2813,12 +3937,61 @@ dependencies = [
]

[[package]]
+
name = "jni"
+
version = "0.22.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+
dependencies = [
+
 "cfg-if",
+
 "combine",
+
 "jni-macros",
+
 "jni-sys 0.4.1",
+
 "log",
+
 "simd_cesu8",
+
 "thiserror 2.0.18",
+
 "walkdir",
+
 "windows-link 0.2.1",
+
]
+

+
[[package]]
+
name = "jni-macros"
+
version = "0.22.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "rustc_version",
+
 "simd_cesu8",
+
 "syn 2.0.117",
+
]
+

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

[[package]]
+
name = "jni-sys"
+
version = "0.4.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+
dependencies = [
+
 "jni-sys-macros",
+
]
+

+
[[package]]
+
name = "jni-sys-macros"
+
version = "0.4.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+
dependencies = [
+
 "quote",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
name = "jobserver"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2874,7 +4047,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "serde",
 "unicode-segmentation",
]
@@ -2897,10 +4070,16 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
-
 "spin",
+
 "spin 0.9.8",
]

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

+
[[package]]
name = "lexopt"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2932,9 +4111,9 @@ dependencies = [

[[package]]
name = "libc"
-
version = "0.2.172"
+
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"

[[package]]
name = "libdbus-sys"
@@ -2979,7 +4158,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "libc",
 "redox_syscall",
]
@@ -3034,12 +4213,46 @@ dependencies = [
]

[[package]]
+
name = "loom"
+
version = "0.7.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
+
dependencies = [
+
 "cfg-if",
+
 "generator",
+
 "scoped-tls",
+
 "tracing",
+
 "tracing-subscriber",
+
]
+

+
[[package]]
+
name = "lru"
+
version = "0.18.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
+
dependencies = [
+
 "hashbrown 0.17.1",
+
]
+

+
[[package]]
+
name = "lru-slab"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+

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

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

+
[[package]]
name = "markup5ever"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3072,7 +4285,16 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
+
]
+

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

[[package]]
@@ -3136,13 +4358,42 @@ dependencies = [

[[package]]
name = "mio"
-
version = "1.0.3"
+
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
 "libc",
+
 "log",
 "wasi 0.11.0+wasi-snapshot-preview1",
-
 "windows-sys 0.52.0",
+
 "windows-sys 0.48.0",
+
]
+

+
[[package]]
+
name = "mio"
+
version = "1.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+
dependencies = [
+
 "libc",
+
 "wasi 0.11.0+wasi-snapshot-preview1",
+
 "windows-sys 0.61.2",
+
]
+

+
[[package]]
+
name = "moka"
+
version = "0.12.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046"
+
dependencies = [
+
 "crossbeam-channel",
+
 "crossbeam-epoch",
+
 "crossbeam-utils",
+
 "equivalent",
+
 "parking_lot",
+
 "portable-atomic",
+
 "smallvec",
+
 "tagptr",
+
 "uuid",
]

[[package]]
@@ -3162,43 +4413,244 @@ dependencies = [
 "once_cell",
 "png 0.18.1",
 "serde",
-
 "thiserror 2.0.12",
-
 "windows-sys 0.60.2",
+
 "thiserror 2.0.18",
+
 "windows-sys 0.61.2",
+
]
+

+
[[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 = "multihash"
+
version = "0.19.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "577c63b00ad74d57e8c9aa870b5fccebf2fd64a308a5aee9f1bb88e4aea19447"
+
dependencies = [
+
 "serde",
+
 "unsigned-varint",
+
]
+

+
[[package]]
+
name = "n0-error"
+
version = "1.0.0-rc.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "223e946a84aa91644507a6b7865cfebbb9a231ace499041c747ab0fd30408212"
+
dependencies = [
+
 "n0-error-macros",
+
 "spez",
+
]
+

+
[[package]]
+
name = "n0-error-macros"
+
version = "1.0.0-rc.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "565305a21e6b3bf26640ad98f05a0fda12d3ab4315394566b52a7bddb8b34828"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
+
name = "n0-future"
+
version = "0.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe"
+
dependencies = [
+
 "cfg_aliases",
+
 "derive_more 2.1.1",
+
 "futures-buffered",
+
 "futures-lite",
+
 "futures-util",
+
 "js-sys",
+
 "pin-project",
+
 "send_wrapper",
+
 "tokio",
+
 "tokio-util",
+
 "wasm-bindgen",
+
 "wasm-bindgen-futures",
+
 "web-time",
+
]
+

+
[[package]]
+
name = "n0-watcher"
+
version = "1.0.0-rc.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "928d8039a66cce5efcfd35e88b32d3defc8eba630b3ac451522997f563956a52"
+
dependencies = [
+
 "derive_more 2.1.1",
+
 "n0-error",
+
 "n0-future",
+
]
+

+
[[package]]
+
name = "ndk"
+
version = "0.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+
dependencies = [
+
 "bitflags 2.11.1",
+
 "jni-sys 0.3.0",
+
 "log",
+
 "ndk-sys",
+
 "num_enum",
+
 "raw-window-handle",
+
 "thiserror 1.0.69",
+
]
+

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

+
[[package]]
+
name = "ndk-sys"
+
version = "0.6.0+11769913"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+
dependencies = [
+
 "jni-sys 0.3.0",
+
]
+

+
[[package]]
+
name = "nested_enum_utils"
+
version = "0.2.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b1d5475271bdd36a4a2769eac1ef88df0f99428ea43e52dfd8b0ee5cb674695f"
+
dependencies = [
+
 "proc-macro-crate 3.3.0",
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
+
name = "netdev"
+
version = "0.43.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "57bacaf873ee4eab5646f99b381b271ec75e716902a67cf962c0f328c5eb5bfb"
+
dependencies = [
+
 "block2 0.6.1",
+
 "dispatch2",
+
 "dlopen2",
+
 "ipnet",
+
 "libc",
+
 "mac-addr",
+
 "netlink-packet-core",
+
 "netlink-packet-route 0.29.0",
+
 "netlink-sys",
+
 "objc2-core-foundation",
+
 "objc2-core-wlan",
+
 "objc2-foundation 0.3.1",
+
 "objc2-system-configuration",
+
 "once_cell",
+
 "plist",
+
 "windows-sys 0.61.2",
+
]
+

+
[[package]]
+
name = "netlink-packet-core"
+
version = "0.8.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4"
+
dependencies = [
+
 "paste",
+
]
+

+
[[package]]
+
name = "netlink-packet-route"
+
version = "0.29.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "df9854ea6ad14e3f4698a7f03b65bce0833dd2d81d594a0e4a984170537146b6"
+
dependencies = [
+
 "bitflags 2.11.1",
+
 "libc",
+
 "log",
+
 "netlink-packet-core",
]

[[package]]
-
name = "multibase"
-
version = "0.9.1"
+
name = "netlink-packet-route"
+
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404"
+
checksum = "be8919612f6028ab4eacbbfe1234a9a43e3722c6e0915e7ff519066991905092"
dependencies = [
-
 "base-x",
-
 "data-encoding",
-
 "data-encoding-macro",
+
 "bitflags 2.11.1",
+
 "libc",
+
 "log",
+
 "netlink-packet-core",
]

[[package]]
-
name = "ndk"
-
version = "0.9.0"
+
name = "netlink-proto"
+
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+
checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128"
dependencies = [
-
 "bitflags 2.9.0",
-
 "jni-sys",
+
 "bytes",
+
 "futures",
 "log",
-
 "ndk-sys",
-
 "num_enum",
-
 "raw-window-handle",
-
 "thiserror 1.0.69",
+
 "netlink-packet-core",
+
 "netlink-sys",
+
 "thiserror 2.0.18",
]

[[package]]
-
name = "ndk-sys"
-
version = "0.6.0+11769913"
+
name = "netlink-sys"
+
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+
checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae"
+
dependencies = [
+
 "bytes",
+
 "futures-util",
+
 "libc",
+
 "log",
+
 "tokio",
+
]
+

+
[[package]]
+
name = "netwatch"
+
version = "0.17.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b5bfbba77b994ce69f1d40fc66fd8abbd23df62ce4aea61fbb34d638106a2549"
dependencies = [
-
 "jni-sys",
+
 "atomic-waker",
+
 "bytes",
+
 "cfg_aliases",
+
 "derive_more 2.1.1",
+
 "js-sys",
+
 "libc",
+
 "n0-error",
+
 "n0-future",
+
 "n0-watcher",
+
 "netdev",
+
 "netlink-packet-core",
+
 "netlink-packet-route 0.30.0",
+
 "netlink-proto",
+
 "netlink-sys",
+
 "noq-udp",
+
 "objc2-core-foundation",
+
 "objc2-system-configuration",
+
 "pin-project-lite",
+
 "serde",
+
 "socket2",
+
 "time",
+
 "tokio",
+
 "tokio-util",
+
 "tracing",
+
 "web-sys",
+
 "windows 0.62.2",
+
 "windows-result 0.4.1",
+
 "wmi",
]

[[package]]
@@ -3208,6 +4660,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"

[[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 = "nodrop"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3239,6 +4700,79 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549e471b99ccaf2f89101bec68f4d244457d5a95a9c3d0672e9564124397741d"

[[package]]
+
name = "noq"
+
version = "1.0.0-rc.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "22739e0831e40f5ab7d6ac5317ed80bfe5fb3f44be57d23fa2eea8bff83fb303"
+
dependencies = [
+
 "bytes",
+
 "cfg_aliases",
+
 "derive_more 2.1.1",
+
 "noq-proto",
+
 "noq-udp",
+
 "pin-project-lite",
+
 "rustc-hash",
+
 "rustls",
+
 "socket2",
+
 "thiserror 2.0.18",
+
 "tokio",
+
 "tokio-stream",
+
 "tracing",
+
 "web-time",
+
]
+

+
[[package]]
+
name = "noq-proto"
+
version = "1.0.0-rc.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7cee32450cf726b223ac4154003c93cb52fbde159ab1240990e88945bf3ae35e"
+
dependencies = [
+
 "aes-gcm",
+
 "bytes",
+
 "derive_more 2.1.1",
+
 "enum-assoc",
+
 "fastbloom",
+
 "getrandom 0.4.2",
+
 "identity-hash",
+
 "lru-slab",
+
 "rand 0.10.1",
+
 "rand_pcg 0.10.2",
+
 "ring",
+
 "rustc-hash",
+
 "rustls",
+
 "rustls-pki-types",
+
 "rustls-platform-verifier 0.7.0",
+
 "slab",
+
 "sorted-index-buffer",
+
 "thiserror 2.0.18",
+
 "tinyvec",
+
 "tracing",
+
 "web-time",
+
]
+

+
[[package]]
+
name = "noq-udp"
+
version = "1.0.0-rc.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "78633d1fe1bde91d12bcabb230ac9edb890857414c6d44f3212e0d309525b5ff"
+
dependencies = [
+
 "cfg_aliases",
+
 "libc",
+
 "socket2",
+
 "tracing",
+
 "windows-sys 0.61.2",
+
]
+

+
[[package]]
+
name = "nu-ansi-term"
+
version = "0.50.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+
dependencies = [
+
 "windows-sys 0.61.2",
+
]
+

+
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3268,9 +4802,9 @@ dependencies = [

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

[[package]]
name = "num-integer"
@@ -3320,7 +4854,7 @@ dependencies = [
 "proc-macro-crate 3.3.0",
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -3333,6 +4867,12 @@ dependencies = [
]

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

+
[[package]]
name = "objc-sys"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3364,7 +4904,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "block2 0.6.1",
 "objc2 0.6.4",
 "objc2-core-foundation",
@@ -3378,7 +4918,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "objc2 0.6.4",
 "objc2-foundation 0.3.1",
]
@@ -3395,12 +4935,14 @@ dependencies = [

[[package]]
name = "objc2-core-foundation"
-
version = "0.3.1"
+
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
+
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
+
 "block2 0.6.1",
 "dispatch2",
+
 "libc",
 "objc2 0.6.4",
]

@@ -3410,7 +4952,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "dispatch2",
 "objc2 0.6.4",
 "objc2-core-foundation",
@@ -3438,6 +4980,19 @@ dependencies = [
]

[[package]]
+
name = "objc2-core-wlan"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5b0343dfef1016d82dd3b7e7383c0afd618437eca6fd03d5139a499ad9f97e6b"
+
dependencies = [
+
 "bitflags 2.11.1",
+
 "objc2 0.6.4",
+
 "objc2-core-foundation",
+
 "objc2-foundation 0.3.1",
+
 "objc2-security",
+
]
+

+
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3458,7 +5013,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "block2 0.5.1",
 "libc",
 "objc2 0.5.2",
@@ -3470,7 +5025,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "block2 0.6.1",
 "libc",
 "objc2 0.6.4",
@@ -3483,7 +5038,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "objc2 0.6.4",
 "objc2-core-foundation",
]
@@ -3494,7 +5049,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "block2 0.5.1",
 "objc2 0.5.2",
 "objc2-foundation 0.2.2",
@@ -3506,7 +5061,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "block2 0.5.1",
 "objc2 0.5.2",
 "objc2-foundation 0.2.2",
@@ -3519,19 +5074,44 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "objc2 0.6.4",
 "objc2-core-foundation",
 "objc2-foundation 0.3.1",
]

[[package]]
+
name = "objc2-security"
+
version = "0.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
+
dependencies = [
+
 "bitflags 2.11.1",
+
 "objc2 0.6.4",
+
 "objc2-core-foundation",
+
]
+

+
[[package]]
+
name = "objc2-system-configuration"
+
version = "0.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396"
+
dependencies = [
+
 "bitflags 2.11.1",
+
 "dispatch2",
+
 "libc",
+
 "objc2 0.6.4",
+
 "objc2-core-foundation",
+
 "objc2-security",
+
]
+

+
[[package]]
name = "objc2-ui-kit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "block2 0.6.1",
 "objc2 0.6.4",
 "objc2-cloud-kit",
@@ -3561,7 +5141,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "block2 0.6.1",
 "objc2 0.6.4",
 "objc2-app-kit",
@@ -3570,12 +5150,12 @@ dependencies = [
]

[[package]]
-
name = "object"
-
version = "0.36.7"
+
name = "oid-registry"
+
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+
checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
dependencies = [
-
 "memchr",
+
 "asn1-rs",
]

[[package]]
@@ -3583,6 +5163,10 @@ name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
dependencies = [
+
 "critical-section",
+
 "portable-atomic",
+
]

[[package]]
name = "once_cell_polyfill"
@@ -3609,6 +5193,12 @@ dependencies = [
]

[[package]]
+
name = "openssl-probe"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+

+
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3633,7 +5223,7 @@ dependencies = [
 "ecdsa",
 "elliptic-curve",
 "primeorder",
-
 "sha2",
+
 "sha2 0.10.9",
]

[[package]]
@@ -3645,7 +5235,7 @@ dependencies = [
 "ecdsa",
 "elliptic-curve",
 "primeorder",
-
 "sha2",
+
 "sha2 0.10.9",
]

[[package]]
@@ -3654,12 +5244,12 @@ version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
dependencies = [
-
 "base16ct",
+
 "base16ct 0.2.0",
 "ecdsa",
 "elliptic-curve",
 "primeorder",
 "rand_core 0.6.4",
-
 "sha2",
+
 "sha2 0.10.9",
]

[[package]]
@@ -3688,6 +5278,22 @@ dependencies = [
]

[[package]]
+
name = "papaya"
+
version = "0.2.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "997ee03cd38c01469a7046643714f0ad28880bcb9e6679ff0666e24817ca19b7"
+
dependencies = [
+
 "equivalent",
+
 "seize",
+
]
+

+
[[package]]
+
name = "parking"
+
version = "2.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+

+
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3711,6 +5317,12 @@ dependencies = [
]

[[package]]
+
name = "paste"
+
version = "1.0.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+

+
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3722,7 +5334,17 @@ version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
-
 "digest",
+
 "digest 0.10.7",
+
]
+

+
[[package]]
+
name = "pem"
+
version = "3.0.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
+
dependencies = [
+
 "base64 0.22.1",
+
 "serde_core",
]

[[package]]
@@ -3735,6 +5357,15 @@ dependencies = [
]

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

+
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3751,6 +5382,16 @@ dependencies = [
]

[[package]]
+
name = "pharos"
+
version = "0.5.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414"
+
dependencies = [
+
 "futures",
+
 "rustc_version",
+
]
+

+
[[package]]
name = "phf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3885,7 +5526,7 @@ dependencies = [
 "phf_shared 0.11.3",
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -3898,7 +5539,7 @@ dependencies = [
 "phf_shared 0.13.1",
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -3938,6 +5579,26 @@ dependencies = [
]

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

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

+
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3955,9 +5616,9 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
-
 "der",
-
 "pkcs8",
-
 "spki",
+
 "der 0.7.10",
+
 "pkcs8 0.10.2",
+
 "spki 0.7.3",
]

[[package]]
@@ -3966,8 +5627,18 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
-
 "der",
-
 "spki",
+
 "der 0.7.10",
+
 "spki 0.7.3",
+
]
+

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

[[package]]
@@ -3978,13 +5649,13 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"

[[package]]
name = "plist"
-
version = "1.7.1"
+
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d"
+
checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
dependencies = [
 "base64 0.22.1",
 "indexmap 2.9.0",
-
 "quick-xml 0.32.0",
+
 "quick-xml 0.39.3",
 "serde",
 "time",
]
@@ -4006,36 +5677,109 @@ dependencies = [
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+
dependencies = [
+
 "bitflags 2.11.1",
+
 "crc32fast",
+
 "fdeflate",
+
 "flate2",
+
 "miniz_oxide",
+
]
+

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

+
[[package]]
+
name = "portable-atomic"
+
version = "1.13.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
dependencies = [
+
 "serde",
+
]
+

+
[[package]]
+
name = "portmapper"
+
version = "0.17.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "aec2a8809e3f7dba624776bb223da9fed49c413c60b3bef21aadcb67a5e35944"
+
dependencies = [
+
 "base64 0.22.1",
+
 "bytes",
+
 "derive_more 2.1.1",
+
 "hyper-util",
+
 "igd-next",
+
 "iroh-metrics",
+
 "libc",
+
 "n0-error",
+
 "n0-future",
+
 "netwatch",
+
 "num_enum",
+
 "rand 0.10.1",
+
 "serde",
+
 "smallvec",
+
 "socket2",
+
 "time",
+
 "tokio",
+
 "tokio-util",
+
 "tower-layer",
+
 "tracing",
+
 "url",
+
]
+

+
[[package]]
+
name = "positioned-io"
+
version = "0.3.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d4ec4b80060f033312b99b6874025d9503d2af87aef2dd4c516e253fbfcdada7"
dependencies = [
-
 "bitflags 2.9.0",
-
 "crc32fast",
-
 "fdeflate",
-
 "flate2",
-
 "miniz_oxide",
+
 "libc",
+
 "winapi",
]

[[package]]
-
name = "poly1305"
-
version = "0.8.0"
+
name = "postcard"
+
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
dependencies = [
-
 "cpufeatures",
-
 "opaque-debug",
-
 "universal-hash",
+
 "cobs",
+
 "embedded-io 0.4.0",
+
 "embedded-io 0.6.1",
+
 "heapless",
+
 "postcard-derive",
+
 "serde",
]

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

[[package]]
@@ -4069,6 +5813,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"

[[package]]
+
name = "prefix-trie"
+
version = "0.8.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"
+
dependencies = [
+
 "either",
+
 "ipnet",
+
 "num-traits",
+
]
+

+
[[package]]
+
name = "prettyplease"
+
version = "0.2.37"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+
dependencies = [
+
 "proc-macro2",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
name = "primeorder"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4107,14 +5872,40 @@ dependencies = [

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

+
[[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-macro-error-attr 1.0.4",
+
 "proc-macro2",
+
 "quote",
+
 "syn 1.0.109",
+
 "version_check",
+
]
+

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

@@ -4148,7 +5939,7 @@ dependencies = [
 "proc-macro-error-attr2",
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -4206,18 +5997,18 @@ dependencies = [

[[package]]
name = "quick-xml"
-
version = "0.32.0"
+
version = "0.37.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2"
+
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
dependencies = [
 "memchr",
]

[[package]]
name = "quick-xml"
-
version = "0.37.5"
+
version = "0.39.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+
checksum = "721da970c312655cde9b4ffe0547f20a8494866a4af5ff51f18b7c633d0c870b"
dependencies = [
 "memchr",
]
@@ -4238,6 +6029,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"

[[package]]
+
name = "r-efi"
+
version = "6.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+

+
[[package]]
name = "radicle"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4270,12 +6067,41 @@ dependencies = [
 "siphasher 1.0.1",
 "sqlite",
 "tempfile",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "uds_windows",
 "unicode-normalization",
]

[[package]]
+
name = "radicle-artifact"
+
version = "0.13.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "61b574a0eec0ee16f5a57394b89a638eaac0a14b55ae014cebc049c369746af7"
+
dependencies = [
+
 "blake3",
+
 "bytes",
+
 "cid",
+
 "clap",
+
 "dunce",
+
 "indexmap 2.9.0",
+
 "indicatif",
+
 "inquire",
+
 "iroh",
+
 "iroh-blobs",
+
 "multihash",
+
 "n0-future",
+
 "nonempty 0.11.0",
+
 "radicle",
+
 "serde",
+
 "serde_json",
+
 "thiserror 2.0.18",
+
 "tokio",
+
 "ureq",
+
 "url",
+
 "walkdir",
+
]
+

+
[[package]]
name = "radicle-cob"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4293,7 +6119,7 @@ dependencies = [
 "serde",
 "serde_json",
 "signature 2.2.0",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
]

[[package]]
@@ -4310,7 +6136,7 @@ dependencies = [
 "schemars 1.2.1",
 "serde",
 "sqlite",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
]

[[package]]
@@ -4331,7 +6157,7 @@ dependencies = [
 "sqlite",
 "ssh-agent-lib",
 "ssh-key",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "winpipe",
 "zeroize",
]
@@ -4365,7 +6191,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db7817cae00f00f0e9a804b08e32d7846e97ae66fff8114d5a0d8c848de80ad5"
dependencies = [
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
]

[[package]]
@@ -4393,7 +6219,7 @@ dependencies = [
 "radicle",
 "serde",
 "serde_json",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "url",
 "uuid",
]
@@ -4467,7 +6293,7 @@ dependencies = [
 "tauri-plugin-log",
 "tauri-plugin-shell",
 "tauri-plugin-window-state",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "tokio",
 "ts-rs",
 "zeroize",
@@ -4480,11 +6306,16 @@ dependencies = [
 "anyhow",
 "axum",
 "base64 0.22.1",
+
 "cid",
+
 "futures-lite",
 "git2",
 "infer",
+
 "iroh",
+
 "iroh-blobs",
 "log",
 "mime-infer",
 "radicle",
+
 "radicle-artifact",
 "radicle-job",
 "radicle-localtime",
 "radicle-surf",
@@ -4496,7 +6327,7 @@ dependencies = [
 "tauri-plugin-clipboard-manager",
 "tauri-plugin-fs",
 "tempfile",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "tree-sitter-bash",
 "tree-sitter-c",
 "tree-sitter-css",
@@ -4516,6 +6347,7 @@ dependencies = [
 "tree-sitter-toml-ng",
 "tree-sitter-typescript",
 "ts-rs",
+
 "ureq",
 "url",
]

@@ -4536,7 +6368,7 @@ dependencies = [
 "rand_chacha 0.2.2",
 "rand_core 0.5.1",
 "rand_hc",
-
 "rand_pcg",
+
 "rand_pcg 0.2.1",
]

[[package]]
@@ -4551,6 +6383,17 @@ dependencies = [
]

[[package]]
+
name = "rand"
+
version = "0.10.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
+
dependencies = [
+
 "chacha20 0.10.0",
+
 "getrandom 0.4.2",
+
 "rand_core 0.10.1",
+
]
+

+
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4589,6 +6432,12 @@ dependencies = [
]

[[package]]
+
name = "rand_core"
+
version = "0.10.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+

+
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4607,18 +6456,63 @@ dependencies = [
]

[[package]]
+
name = "rand_pcg"
+
version = "0.10.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+
dependencies = [
+
 "rand_core 0.10.1",
+
]
+

+
[[package]]
+
name = "range-collections"
+
version = "0.4.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "861706ea9c4aded7584c5cd1d241cec2ea7f5f50999f236c22b65409a1f1a0d0"
+
dependencies = [
+
 "binary-merge",
+
 "inplace-vec-builder",
+
 "ref-cast",
+
 "serde",
+
 "smallvec",
+
]
+

+
[[package]]
name = "raw-window-handle"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"

[[package]]
+
name = "rcgen"
+
version = "0.14.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e"
+
dependencies = [
+
 "pem",
+
 "ring",
+
 "rustls-pki-types",
+
 "time",
+
 "x509-parser",
+
 "yasna",
+
]
+

+
[[package]]
+
name = "redb"
+
version = "4.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8e925444704b5f17d32bf42f5b6e2df050bceebc3dcd6e71cc73dafe8092e839"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
name = "redox_syscall"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
]

[[package]]
@@ -4629,7 +6523,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
 "getrandom 0.2.16",
 "libredox",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
]

[[package]]
@@ -4649,7 +6543,19 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
+
name = "reflink-copy"
+
version = "0.1.29"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "13362233b147e57674c37b802d216b7c5e3dcccbed8967c84f0d8d223868ae27"
+
dependencies = [
+
 "cfg-if",
+
 "libc",
+
 "rustix 1.0.7",
+
 "windows 0.62.2",
]

[[package]]
@@ -4704,15 +6610,20 @@ dependencies = [
 "http-body",
 "http-body-util",
 "hyper",
+
 "hyper-rustls",
 "hyper-util",
 "js-sys",
 "log",
 "percent-encoding",
 "pin-project-lite",
+
 "rustls",
+
 "rustls-pki-types",
+
 "rustls-platform-verifier 0.6.2",
 "serde",
 "serde_json",
 "sync_wrapper",
 "tokio",
+
 "tokio-rustls",
 "tokio-util",
 "tower",
 "tower-http",
@@ -4725,6 +6636,12 @@ dependencies = [
]

[[package]]
+
name = "resolv-conf"
+
version = "0.7.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
+

+
[[package]]
name = "rfc6979"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4759,6 +6676,20 @@ dependencies = [
]

[[package]]
+
name = "ring"
+
version = "0.17.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+
dependencies = [
+
 "cc",
+
 "cfg-if",
+
 "getrandom 0.2.16",
+
 "libc",
+
 "untrusted",
+
 "windows-sys 0.52.0",
+
]
+

+
[[package]]
name = "rkyv"
version = "0.7.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4793,17 +6724,17 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
dependencies = [
-
 "const-oid",
-
 "digest",
+
 "const-oid 0.9.6",
+
 "digest 0.10.7",
 "num-bigint-dig",
 "num-integer",
 "num-traits",
 "pkcs1",
-
 "pkcs8",
+
 "pkcs8 0.10.2",
 "rand_core 0.6.4",
-
 "sha2",
+
 "sha2 0.10.9",
 "signature 2.2.0",
-
 "spki",
+
 "spki 0.7.3",
 "subtle",
 "zeroize",
]
@@ -4825,12 +6756,6 @@ dependencies = [
]

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

-
[[package]]
name = "rustc-hash"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4846,12 +6771,21 @@ dependencies = [
]

[[package]]
+
name = "rusticata-macros"
+
version = "4.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
+
dependencies = [
+
 "nom",
+
]
+

+
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "errno",
 "libc",
 "linux-raw-sys 0.4.15",
@@ -4864,7 +6798,7 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "errno",
 "libc",
 "linux-raw-sys 0.9.4",
@@ -4872,10 +6806,106 @@ dependencies = [
]

[[package]]
+
name = "rustls"
+
version = "0.23.40"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
+
dependencies = [
+
 "log",
+
 "once_cell",
+
 "ring",
+
 "rustls-pki-types",
+
 "rustls-webpki",
+
 "subtle",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "rustls-native-certs"
+
version = "0.8.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
+
dependencies = [
+
 "openssl-probe",
+
 "rustls-pki-types",
+
 "schannel",
+
 "security-framework",
+
]
+

+
[[package]]
+
name = "rustls-pki-types"
+
version = "1.14.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+
dependencies = [
+
 "web-time",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "rustls-platform-verifier"
+
version = "0.6.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
+
dependencies = [
+
 "core-foundation 0.10.0",
+
 "core-foundation-sys",
+
 "jni 0.21.1",
+
 "log",
+
 "once_cell",
+
 "rustls",
+
 "rustls-native-certs",
+
 "rustls-platform-verifier-android",
+
 "rustls-webpki",
+
 "security-framework",
+
 "security-framework-sys",
+
 "webpki-root-certs",
+
 "windows-sys 0.61.2",
+
]
+

+
[[package]]
+
name = "rustls-platform-verifier"
+
version = "0.7.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+
dependencies = [
+
 "core-foundation 0.10.0",
+
 "core-foundation-sys",
+
 "jni 0.22.4",
+
 "log",
+
 "once_cell",
+
 "rustls",
+
 "rustls-native-certs",
+
 "rustls-platform-verifier-android",
+
 "rustls-webpki",
+
 "security-framework",
+
 "security-framework-sys",
+
 "webpki-root-certs",
+
 "windows-sys 0.61.2",
+
]
+

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

+
[[package]]
+
name = "rustls-webpki"
+
version = "0.103.13"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+
dependencies = [
+
 "ring",
+
 "rustls-pki-types",
+
 "untrusted",
+
]
+

+
[[package]]
name = "rustversion"
-
version = "1.0.20"
+
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
+
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"

[[package]]
name = "ryu"
@@ -4893,6 +6923,15 @@ dependencies = [
]

[[package]]
+
name = "schannel"
+
version = "0.1.29"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+
dependencies = [
+
 "windows-sys 0.61.2",
+
]
+

+
[[package]]
name = "schemars"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4929,7 +6968,7 @@ dependencies = [
 "proc-macro2",
 "quote",
 "serde_derive_internals",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -4941,7 +6980,7 @@ dependencies = [
 "proc-macro2",
 "quote",
 "serde_derive_internals",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -4968,10 +7007,10 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
-
 "base16ct",
-
 "der",
+
 "base16ct 0.2.0",
+
 "der 0.7.10",
 "generic-array",
-
 "pkcs8",
+
 "pkcs8 0.10.2",
 "subtle",
 "zeroize",
]
@@ -4986,6 +7025,39 @@ dependencies = [
]

[[package]]
+
name = "security-framework"
+
version = "3.7.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+
dependencies = [
+
 "bitflags 2.11.1",
+
 "core-foundation 0.10.0",
+
 "core-foundation-sys",
+
 "libc",
+
 "security-framework-sys",
+
]
+

+
[[package]]
+
name = "security-framework-sys"
+
version = "2.17.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+
dependencies = [
+
 "core-foundation-sys",
+
 "libc",
+
]
+

+
[[package]]
+
name = "seize"
+
version = "0.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521"
+
dependencies = [
+
 "libc",
+
 "windows-sys 0.61.2",
+
]
+

+
[[package]]
name = "selectors"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5009,7 +7081,7 @@ version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "cssparser 0.36.0",
 "derive_more 2.1.1",
 "log",
@@ -5023,6 +7095,12 @@ dependencies = [
]

[[package]]
+
name = "self_cell"
+
version = "1.2.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
+

+
[[package]]
name = "semver"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5032,6 +7110,12 @@ dependencies = [
]

[[package]]
+
name = "send_wrapper"
+
version = "0.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
+

+
[[package]]
name = "seq-macro"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5051,11 +7135,21 @@ dependencies = [
name = "serde-untagged"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e"
+
checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e"
+
dependencies = [
+
 "erased-serde",
+
 "serde",
+
 "typeid",
+
]
+

+
[[package]]
+
name = "serde_bytes"
+
version = "0.11.19"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
dependencies = [
-
 "erased-serde",
 "serde",
-
 "typeid",
+
 "serde_core",
]

[[package]]
@@ -5075,7 +7169,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -5086,7 +7180,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -5120,7 +7214,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -5180,7 +7274,17 @@ dependencies = [
 "darling",
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
+
name = "serdect"
+
version = "0.4.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e"
+
dependencies = [
+
 "base16ct 1.0.0",
+
 "serde",
]

[[package]]
@@ -5202,7 +7306,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -5225,14 +7329,40 @@ dependencies = [
]

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

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

+
[[package]]
+
name = "sha2"
+
version = "0.11.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+
dependencies = [
+
 "cfg-if",
+
 "cpufeatures 0.3.0",
+
 "digest 0.11.3",
+
]
+

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

[[package]]
@@ -5252,6 +7382,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"

[[package]]
+
name = "signal-hook"
+
version = "0.3.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
+
dependencies = [
+
 "libc",
+
 "signal-hook-registry",
+
]
+

+
[[package]]
+
name = "signal-hook-mio"
+
version = "0.2.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
+
dependencies = [
+
 "libc",
+
 "mio 0.8.11",
+
 "signal-hook",
+
]
+

+
[[package]]
+
name = "signal-hook-registry"
+
version = "1.4.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+
dependencies = [
+
 "errno",
+
 "libc",
+
]
+

+
[[package]]
name = "signature"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5263,23 +7424,48 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
-
 "digest",
+
 "digest 0.10.7",
 "rand_core 0.6.4",
]

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

+
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"

[[package]]
+
name = "simd_cesu8"
+
version = "1.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
+
dependencies = [
+
 "rustc_version",
+
 "simdutf8",
+
]
+

+
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"

[[package]]
+
name = "simple-dns"
+
version = "0.11.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "df350943049174c4ae8ced56c604e28270258faec12a6a48637a7655287c9ce0"
+
dependencies = [
+
 "bitflags 2.11.1",
+
]
+

+
[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5305,6 +7491,9 @@ name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
+
dependencies = [
+
 "serde",
+
]

[[package]]
name = "smartstring"
@@ -5319,12 +7508,12 @@ dependencies = [

[[package]]
name = "socket2"
-
version = "0.5.9"
+
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
+
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
 "libc",
-
 "windows-sys 0.52.0",
+
 "windows-sys 0.61.2",
]

[[package]]
@@ -5360,6 +7549,12 @@ dependencies = [
]

[[package]]
+
name = "sorted-index-buffer"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06"
+

+
[[package]]
name = "soup3"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5386,10 +7581,30 @@ dependencies = [
]

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

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

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

[[package]]
name = "spki"
@@ -5398,7 +7613,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
 "base64ct",
-
 "der",
+
 "der 0.7.10",
+
]
+

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

[[package]]
@@ -5442,7 +7667,7 @@ dependencies = [
 "ssh-encoding",
 "ssh-key",
 "subtle",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
]

[[package]]
@@ -5454,7 +7679,7 @@ dependencies = [
 "aes",
 "aes-gcm",
 "cbc",
-
 "chacha20",
+
 "chacha20 0.9.1",
 "cipher",
 "ctr",
 "poly1305",
@@ -5469,8 +7694,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
dependencies = [
 "base64ct",
-
 "pem-rfc7468",
-
 "sha2",
+
 "pem-rfc7468 0.7.0",
+
 "sha2 0.10.9",
]

[[package]]
@@ -5480,7 +7705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3"
dependencies = [
 "bcrypt-pbkdf",
-
 "ed25519-dalek",
+
 "ed25519-dalek 2.2.0",
 "num-bigint-dig",
 "p256",
 "p384",
@@ -5488,7 +7713,7 @@ dependencies = [
 "rand_core 0.6.4",
 "rsa",
 "sec1",
-
 "sha2",
+
 "sha2 0.10.9",
 "signature 2.2.0",
 "ssh-cipher",
 "ssh-encoding",
@@ -5584,7 +7809,7 @@ checksum = "ae36a4951ca7bd1cfd991c241584a9824a70f6aff1e7d4f693fb3f2465e4030e"
dependencies = [
 "quote",
 "swc_macros_common",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -5594,6 +7819,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"

[[package]]
+
name = "strum"
+
version = "0.28.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
+
dependencies = [
+
 "strum_macros",
+
]
+

+
[[package]]
+
name = "strum_macros"
+
version = "0.28.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
+
dependencies = [
+
 "heck 0.5.0",
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5632,7 +7878,7 @@ dependencies = [
 "swc_eq_ignore_macros",
 "swc_visit",
 "tracing",
-
 "unicode-width",
+
 "unicode-width 0.2.2",
 "url",
]

@@ -5642,7 +7888,7 @@ version = "18.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a573a0c72850dec8d4d8085f152d5778af35a2520c3093b242d2d1d50776da7c"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "is-macro",
 "num-bigint",
 "once_cell",
@@ -5662,7 +7908,7 @@ version = "26.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e82f7747e052c6ff6e111fa4adeb14e33b46ee6e94fe5ef717601f651db48fc"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "either",
 "num-bigint",
 "rustc-hash",
@@ -5684,7 +7930,7 @@ version = "27.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f1a51af1a92cd4904c073b293e491bbc0918400a45d58227b34c961dd6f52d7"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "either",
 "num-bigint",
 "phf 0.11.3",
@@ -5707,7 +7953,7 @@ checksum = "c16ce73424a6316e95e09065ba6a207eba7765496fed113702278b7711d4b632"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -5718,7 +7964,7 @@ checksum = "aae1efbaa74943dc5ad2a2fb16cbd78b77d7e4d63188f3c5b4df2b4dcd2faaae"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -5755,9 +8001,9 @@ dependencies = [

[[package]]
name = "syn"
-
version = "2.0.101"
+
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
+
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
 "proc-macro2",
 "quote",
@@ -5765,6 +8011,17 @@ dependencies = [
]

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

+
[[package]]
name = "sync-ptr"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5787,7 +8044,28 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
+
name = "system-configuration"
+
version = "0.7.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
+
dependencies = [
+
 "bitflags 2.11.1",
+
 "core-foundation 0.9.4",
+
 "system-configuration-sys",
+
]
+

+
[[package]]
+
name = "system-configuration-sys"
+
version = "0.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+
dependencies = [
+
 "core-foundation-sys",
+
 "libc",
]

[[package]]
@@ -5804,14 +8082,20 @@ dependencies = [
]

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

+
[[package]]
name = "tao"
version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "block2 0.6.1",
-
 "core-foundation",
+
 "core-foundation 0.10.0",
 "core-graphics 0.25.0",
 "crossbeam-channel",
 "dbus",
@@ -5821,7 +8105,7 @@ dependencies = [
 "gdkwayland-sys",
 "gdkx11-sys",
 "gtk",
-
 "jni",
+
 "jni 0.21.1",
 "libc",
 "log",
 "ndk",
@@ -5851,7 +8135,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -5894,7 +8178,7 @@ dependencies = [
 "gtk",
 "heck 0.5.0",
 "http",
-
 "jni",
+
 "jni 0.21.1",
 "libc",
 "log",
 "mime",
@@ -5918,7 +8202,7 @@ dependencies = [
 "tauri-runtime",
 "tauri-runtime-wry",
 "tauri-utils",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "tokio",
 "tray-icon",
 "url",
@@ -5968,10 +8252,10 @@ dependencies = [
 "semver",
 "serde",
 "serde_json",
-
 "sha2",
-
 "syn 2.0.101",
+
 "sha2 0.10.9",
+
 "syn 2.0.117",
 "tauri-utils",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "time",
 "url",
 "uuid",
@@ -5987,7 +8271,7 @@ dependencies = [
 "heck 0.5.0",
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
 "tauri-codegen",
 "tauri-utils",
]
@@ -6021,7 +8305,7 @@ dependencies = [
 "serde_json",
 "tauri",
 "tauri-plugin",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
]

[[package]]
@@ -6038,7 +8322,7 @@ dependencies = [
 "tauri",
 "tauri-plugin",
 "tauri-plugin-fs",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "url",
]

@@ -6061,7 +8345,7 @@ dependencies = [
 "tauri",
 "tauri-plugin",
 "tauri-utils",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "toml 0.9.6",
 "url",
]
@@ -6084,7 +8368,7 @@ dependencies = [
 "swift-rs",
 "tauri",
 "tauri-plugin",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "time",
]

@@ -6105,7 +8389,7 @@ dependencies = [
 "shared_child",
 "tauri",
 "tauri-plugin",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "tokio",
]

@@ -6115,13 +8399,13 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "log",
 "serde",
 "serde_json",
 "tauri",
 "tauri-plugin",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
]

[[package]]
@@ -6134,7 +8418,7 @@ dependencies = [
 "dpi",
 "gtk",
 "http",
-
 "jni",
+
 "jni 0.21.1",
 "objc2 0.6.4",
 "objc2-ui-kit",
 "objc2-web-kit",
@@ -6142,7 +8426,7 @@ dependencies = [
 "serde",
 "serde_json",
 "tauri-utils",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "url",
 "webkit2gtk",
 "webview2-com",
@@ -6157,7 +8441,7 @@ checksum = "a3989df2ae1c476404fe0a2e8ffc4cfbde97e51efd613c2bb5355fbc9ab52cf0"
dependencies = [
 "gtk",
 "http",
-
 "jni",
+
 "jni 0.21.1",
 "log",
 "objc2 0.6.4",
 "objc2-app-kit",
@@ -6210,7 +8494,7 @@ dependencies = [
 "serde_with",
 "serialize-to-javascript",
 "swift-rs",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "toml 0.9.6",
 "url",
 "urlpattern",
@@ -6295,7 +8579,7 @@ dependencies = [
 "radicle-types",
 "serde",
 "serde_json",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "tokio",
 "tower-http",
]
@@ -6320,11 +8604,11 @@ dependencies = [

[[package]]
name = "thiserror"
-
version = "2.0.12"
+
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
-
 "thiserror-impl 2.0.12",
+
 "thiserror-impl 2.0.18",
]

[[package]]
@@ -6335,18 +8619,27 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
name = "thiserror-impl"
-
version = "2.0.12"
+
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
+
]
+

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

[[package]]
@@ -6362,32 +8655,33 @@ dependencies = [

[[package]]
name = "time"
-
version = "0.3.41"
+
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
+
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
 "deranged",
 "itoa",
+
 "js-sys",
 "libc",
 "num-conv",
 "num_threads",
 "powerfmt",
-
 "serde",
+
 "serde_core",
 "time-core",
 "time-macros",
]

[[package]]
name = "time-core"
-
version = "0.1.4"
+
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
+
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"

[[package]]
name = "time-macros"
-
version = "0.2.22"
+
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
+
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
 "num-conv",
 "time-core",
@@ -6420,29 +8714,51 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"

[[package]]
name = "tokio"
-
version = "1.45.0"
+
version = "1.52.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
+
checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"
dependencies = [
-
 "backtrace",
 "bytes",
 "libc",
-
 "mio",
+
 "mio 1.2.0",
 "pin-project-lite",
+
 "signal-hook-registry",
 "socket2",
 "tokio-macros",
-
 "windows-sys 0.52.0",
+
 "windows-sys 0.61.2",
]

[[package]]
name = "tokio-macros"
-
version = "2.5.0"
+
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
+
]
+

+
[[package]]
+
name = "tokio-rustls"
+
version = "0.26.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+
dependencies = [
+
 "rustls",
+
 "tokio",
+
]
+

+
[[package]]
+
name = "tokio-stream"
+
version = "0.1.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
+
dependencies = [
+
 "futures-core",
+
 "pin-project-lite",
+
 "tokio",
+
 "tokio-util",
]

[[package]]
@@ -6454,11 +8770,36 @@ dependencies = [
 "bytes",
 "futures-core",
 "futures-sink",
+
 "futures-util",
+
 "hashbrown 0.15.3",
 "pin-project-lite",
 "tokio",
]

[[package]]
+
name = "tokio-websockets"
+
version = "0.13.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dad543404f98bfc969aeb71994105c592acfc6c43323fddcd016bb208d1c65cb"
+
dependencies = [
+
 "base64 0.22.1",
+
 "bytes",
+
 "futures-core",
+
 "futures-sink",
+
 "getrandom 0.4.2",
+
 "http",
+
 "httparse",
+
 "rand 0.10.1",
+
 "ring",
+
 "rustls-pki-types",
+
 "sha1_smol",
+
 "simdutf8",
+
 "tokio",
+
 "tokio-rustls",
+
 "tokio-util",
+
]
+

+
[[package]]
name = "toml"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6581,7 +8922,7 @@ version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "bytes",
 "futures-util",
 "http",
@@ -6607,10 +8948,11 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"

[[package]]
name = "tracing"
-
version = "0.1.41"
+
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
+
 "log",
 "pin-project-lite",
 "tracing-attributes",
 "tracing-core",
@@ -6618,22 +8960,52 @@ dependencies = [

[[package]]
name = "tracing-attributes"
-
version = "0.1.28"
+
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

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

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

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

[[package]]
@@ -6654,8 +9026,8 @@ dependencies = [
 "once_cell",
 "png 0.18.1",
 "serde",
-
 "thiserror 2.0.12",
-
 "windows-sys 0.60.2",
+
 "thiserror 2.0.18",
+
 "windows-sys 0.61.2",
]

[[package]]
@@ -6730,7 +9102,7 @@ checksum = "076673d82b859652de3e7abe73a4592c173e51dfc9b83eb49f0479fd9fe4631c"
dependencies = [
 "regex",
 "streaming-iterator",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "tree-sitter",
]

@@ -6897,7 +9269,7 @@ checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
dependencies = [
 "dprint-plugin-typescript",
 "serde_json",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "ts-rs-macros",
]

@@ -6909,7 +9281,7 @@ checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
 "termcolor",
]

@@ -6921,9 +9293,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"

[[package]]
name = "typenum"
-
version = "1.18.0"
+
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"

[[package]]
name = "uds_windows"
@@ -7012,21 +9384,74 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"

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

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

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

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

[[package]]
+
name = "unsigned-varint"
+
version = "0.8.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06"
+

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

+
[[package]]
+
name = "ureq"
+
version = "3.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
+
dependencies = [
+
 "base64 0.22.1",
+
 "flate2",
+
 "log",
+
 "percent-encoding",
+
 "rustls",
+
 "rustls-pki-types",
+
 "ureq-proto",
+
 "utf8-zero",
+
 "webpki-roots",
+
]
+

+
[[package]]
+
name = "ureq-proto"
+
version = "0.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
+
dependencies = [
+
 "base64 0.22.1",
+
 "http",
+
 "httparse",
+
 "log",
+
]
+

+
[[package]]
name = "url"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7060,7 +9485,13 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
name = "utf8-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
+
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
+

+
[[package]]
+
name = "utf8-zero"
+
version = "0.8.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"

[[package]]
name = "utf8_iter"
@@ -7087,6 +9518,12 @@ dependencies = [
]

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

+
[[package]]
name = "value-bag"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7099,6 +9536,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"

[[package]]
+
name = "vergen"
+
version = "9.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75"
+
dependencies = [
+
 "anyhow",
+
 "derive_builder",
+
 "rustversion",
+
 "vergen-lib",
+
]
+

+
[[package]]
+
name = "vergen-gitcl"
+
version = "9.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "77ff3b5300a085d6bcd8fc96a507f706a28ae3814693236c9b409db71a1d15b9"
+
dependencies = [
+
 "anyhow",
+
 "derive_builder",
+
 "rustversion",
+
 "time",
+
 "vergen",
+
 "vergen-lib",
+
]
+

+
[[package]]
+
name = "vergen-lib"
+
version = "9.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569"
+
dependencies = [
+
 "anyhow",
+
 "derive_builder",
+
 "rustversion",
+
]
+

+
[[package]]
name = "version-compare"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7171,6 +9645,24 @@ dependencies = [
]

[[package]]
+
name = "wasip2"
+
version = "1.0.3+wasi-0.2.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+
dependencies = [
+
 "wit-bindgen 0.57.1",
+
]
+

+
[[package]]
+
name = "wasip3"
+
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+
dependencies = [
+
 "wit-bindgen 0.51.0",
+
]
+

+
[[package]]
name = "wasm-bindgen"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7212,7 +9704,7 @@ dependencies = [
 "bumpalo",
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
 "wasm-bindgen-shared",
]

@@ -7226,6 +9718,28 @@ dependencies = [
]

[[package]]
+
name = "wasm-encoder"
+
version = "0.244.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+
dependencies = [
+
 "leb128fmt",
+
 "wasmparser",
+
]
+

+
[[package]]
+
name = "wasm-metadata"
+
version = "0.244.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+
dependencies = [
+
 "anyhow",
+
 "indexmap 2.9.0",
+
 "wasm-encoder",
+
 "wasmparser",
+
]
+

+
[[package]]
name = "wasm-streams"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7239,6 +9753,18 @@ dependencies = [
]

[[package]]
+
name = "wasmparser"
+
version = "0.244.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+
dependencies = [
+
 "bitflags 2.11.1",
+
 "hashbrown 0.15.3",
+
 "indexmap 2.9.0",
+
 "semver",
+
]
+

+
[[package]]
name = "wayland-backend"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7257,7 +9783,7 @@ version = "0.31.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "rustix 0.38.44",
 "wayland-backend",
 "wayland-scanner",
@@ -7269,7 +9795,7 @@ version = "0.32.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "wayland-backend",
 "wayland-client",
 "wayland-scanner",
@@ -7281,7 +9807,7 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
 "wayland-backend",
 "wayland-client",
 "wayland-protocols",
@@ -7319,6 +9845,16 @@ dependencies = [
]

[[package]]
+
name = "web-time"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+
dependencies = [
+
 "js-sys",
+
 "wasm-bindgen",
+
]
+

+
[[package]]
name = "web_atoms"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7375,6 +9911,24 @@ dependencies = [
]

[[package]]
+
name = "webpki-root-certs"
+
version = "1.0.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
+
dependencies = [
+
 "rustls-pki-types",
+
]
+

+
[[package]]
+
name = "webpki-roots"
+
version = "1.0.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
+
dependencies = [
+
 "rustls-pki-types",
+
]
+

+
[[package]]
name = "webview2-com"
version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7384,8 +9938,8 @@ dependencies = [
 "webview2-com-sys",
 "windows 0.61.1",
 "windows-core 0.61.0",
-
 "windows-implement 0.60.0",
-
 "windows-interface 0.59.1",
+
 "windows-implement 0.60.2",
+
 "windows-interface 0.59.3",
]

[[package]]
@@ -7396,7 +9950,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -7405,7 +9959,7 @@ version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
dependencies = [
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "windows 0.61.1",
 "windows-core 0.61.0",
]
@@ -7417,6 +9971,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"

[[package]]
+
name = "widestring"
+
version = "1.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
+

+
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7478,11 +10038,23 @@ version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
dependencies = [
-
 "windows-collections",
+
 "windows-collections 0.2.0",
 "windows-core 0.61.0",
-
 "windows-future",
+
 "windows-future 0.2.0",
 "windows-link 0.1.1",
-
 "windows-numerics",
+
 "windows-numerics 0.2.0",
+
]
+

+
[[package]]
+
name = "windows"
+
version = "0.62.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
+
dependencies = [
+
 "windows-collections 0.3.2",
+
 "windows-core 0.62.2",
+
 "windows-future 0.3.2",
+
 "windows-numerics 0.3.1",
]

[[package]]
@@ -7495,6 +10067,15 @@ dependencies = [
]

[[package]]
+
name = "windows-collections"
+
version = "0.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
+
dependencies = [
+
 "windows-core 0.62.2",
+
]
+

+
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7513,14 +10094,27 @@ version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
-
 "windows-implement 0.60.0",
-
 "windows-interface 0.59.1",
+
 "windows-implement 0.60.2",
+
 "windows-interface 0.59.3",
 "windows-link 0.1.1",
 "windows-result 0.3.2",
 "windows-strings 0.4.0",
]

[[package]]
+
name = "windows-core"
+
version = "0.62.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+
dependencies = [
+
 "windows-implement 0.60.2",
+
 "windows-interface 0.59.3",
+
 "windows-link 0.2.1",
+
 "windows-result 0.4.1",
+
 "windows-strings 0.5.1",
+
]
+

+
[[package]]
name = "windows-future"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7531,6 +10125,17 @@ dependencies = [
]

[[package]]
+
name = "windows-future"
+
version = "0.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
+
dependencies = [
+
 "windows-core 0.62.2",
+
 "windows-link 0.2.1",
+
 "windows-threading",
+
]
+

+
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7538,18 +10143,18 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
name = "windows-implement"
-
version = "0.60.0"
+
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -7560,18 +10165,18 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
name = "windows-interface"
-
version = "0.59.1"
+
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -7597,6 +10202,27 @@ dependencies = [
]

[[package]]
+
name = "windows-numerics"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
+
dependencies = [
+
 "windows-core 0.62.2",
+
 "windows-link 0.2.1",
+
]
+

+
[[package]]
+
name = "windows-registry"
+
version = "0.6.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
+
dependencies = [
+
 "windows-link 0.2.1",
+
 "windows-result 0.4.1",
+
 "windows-strings 0.5.1",
+
]
+

+
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7615,6 +10241,15 @@ dependencies = [
]

[[package]]
+
name = "windows-result"
+
version = "0.4.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+
dependencies = [
+
 "windows-link 0.2.1",
+
]
+

+
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7634,6 +10269,15 @@ dependencies = [
]

[[package]]
+
name = "windows-strings"
+
version = "0.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+
dependencies = [
+
 "windows-link 0.2.1",
+
]
+

+
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7751,6 +10395,15 @@ dependencies = [
]

[[package]]
+
name = "windows-threading"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+
dependencies = [
+
 "windows-link 0.2.1",
+
]
+

+
[[package]]
name = "windows-version"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7987,12 +10640,106 @@ dependencies = [
]

[[package]]
+
name = "wit-bindgen"
+
version = "0.51.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+
dependencies = [
+
 "wit-bindgen-rust-macro",
+
]
+

+
[[package]]
+
name = "wit-bindgen"
+
version = "0.57.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+

+
[[package]]
+
name = "wit-bindgen-core"
+
version = "0.51.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+
dependencies = [
+
 "anyhow",
+
 "heck 0.5.0",
+
 "wit-parser",
+
]
+

+
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
-
 "bitflags 2.9.0",
+
 "bitflags 2.11.1",
+
]
+

+
[[package]]
+
name = "wit-bindgen-rust"
+
version = "0.51.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+
dependencies = [
+
 "anyhow",
+
 "heck 0.5.0",
+
 "indexmap 2.9.0",
+
 "prettyplease",
+
 "syn 2.0.117",
+
 "wasm-metadata",
+
 "wit-bindgen-core",
+
 "wit-component",
+
]
+

+
[[package]]
+
name = "wit-bindgen-rust-macro"
+
version = "0.51.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+
dependencies = [
+
 "anyhow",
+
 "prettyplease",
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.117",
+
 "wit-bindgen-core",
+
 "wit-bindgen-rust",
+
]
+

+
[[package]]
+
name = "wit-component"
+
version = "0.244.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+
dependencies = [
+
 "anyhow",
+
 "bitflags 2.11.1",
+
 "indexmap 2.9.0",
+
 "log",
+
 "serde",
+
 "serde_derive",
+
 "serde_json",
+
 "wasm-encoder",
+
 "wasm-metadata",
+
 "wasmparser",
+
 "wit-parser",
+
]
+

+
[[package]]
+
name = "wit-parser"
+
version = "0.244.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+
dependencies = [
+
 "anyhow",
+
 "id-arena",
+
 "indexmap 2.9.0",
+
 "log",
+
 "semver",
+
 "serde",
+
 "serde_derive",
+
 "serde_json",
+
 "unicode-xid",
+
 "wasmparser",
]

[[package]]
@@ -8006,7 +10753,7 @@ dependencies = [
 "os_pipe",
 "rustix 0.38.44",
 "tempfile",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "tree_magic_mini",
 "wayland-backend",
 "wayland-client",
@@ -8015,6 +10762,21 @@ dependencies = [
]

[[package]]
+
name = "wmi"
+
version = "0.18.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64"
+
dependencies = [
+
 "chrono",
+
 "futures",
+
 "log",
+
 "serde",
+
 "thiserror 2.0.18",
+
 "windows 0.62.2",
+
 "windows-core 0.62.2",
+
]
+

+
[[package]]
name = "writeable"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8038,7 +10800,7 @@ dependencies = [
 "gtk",
 "http",
 "javascriptcore-rs",
-
 "jni",
+
 "jni 0.21.1",
 "libc",
 "ndk",
 "objc2 0.6.4",
@@ -8050,10 +10812,10 @@ dependencies = [
 "once_cell",
 "percent-encoding",
 "raw-window-handle",
-
 "sha2",
+
 "sha2 0.10.9",
 "soup3",
 "tao-macros",
-
 "thiserror 2.0.12",
+
 "thiserror 2.0.18",
 "url",
 "webkit2gtk",
 "webkit2gtk-sys",
@@ -8065,6 +10827,25 @@ dependencies = [
]

[[package]]
+
name = "ws_stream_wasm"
+
version = "0.7.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc"
+
dependencies = [
+
 "async_io_stream",
+
 "futures",
+
 "js-sys",
+
 "log",
+
 "pharos",
+
 "rustc_version",
+
 "send_wrapper",
+
 "thiserror 2.0.18",
+
 "wasm-bindgen",
+
 "wasm-bindgen-futures",
+
 "web-sys",
+
]
+

+
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8112,6 +10893,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"

[[package]]
+
name = "x509-parser"
+
version = "0.18.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
+
dependencies = [
+
 "asn1-rs",
+
 "data-encoding",
+
 "der-parser",
+
 "lazy_static",
+
 "nom",
+
 "oid-registry",
+
 "ring",
+
 "rusticata-macros",
+
 "thiserror 2.0.18",
+
 "time",
+
]
+

+
[[package]]
name = "xattr"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8122,6 +10921,30 @@ dependencies = [
]

[[package]]
+
name = "xml-rs"
+
version = "0.8.28"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
+

+
[[package]]
+
name = "xmltree"
+
version = "0.10.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb"
+
dependencies = [
+
 "xml-rs",
+
]
+

+
[[package]]
+
name = "yasna"
+
version = "0.5.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
+
dependencies = [
+
 "time",
+
]
+

+
[[package]]
name = "yoke"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8141,7 +10964,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
 "synstructure",
]

@@ -8162,7 +10985,7 @@ checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]

[[package]]
@@ -8182,17 +11005,29 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
 "synstructure",
]

[[package]]
name = "zeroize"
-
version = "1.8.1"
+
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
dependencies = [
 "serde",
+
 "zeroize_derive",
+
]
+

+
[[package]]
+
name = "zeroize_derive"
+
version = "1.4.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.117",
]

[[package]]
@@ -8225,5 +11060,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
 "proc-macro2",
 "quote",
-
 "syn 2.0.101",
+
 "syn 2.0.117",
]
modified crates/radicle-tauri/Cargo.toml
@@ -4,7 +4,7 @@ version = "0.0.0"
authors = ["Rudolfs Osins <rudolfs@osins.org>", "Sebastian Martinez <me@sebastinez.dev>"]
license = "MIT OR Apache-2.0"
edition = "2021"
-
rust-version = "1.86"
+
rust-version = "1.91"
publish = false

[lib]
modified crates/radicle-tauri/src/commands/cob.rs
@@ -13,6 +13,7 @@ use crate::AppState;
pub mod issue;
pub mod job;
pub mod patch;
+
pub mod release;

#[tauri::command]
pub async fn get_embed(
added crates/radicle-tauri/src/commands/cob/release.rs
@@ -0,0 +1,452 @@
+
use std::path::PathBuf;
+

+
use radicle::git;
+
use radicle::identity::RepoId;
+
use radicle_types::cobs::release;
+
use radicle_types::error::Error;
+
use radicle_types::seeder;
+
use radicle_types::traits::release::Releases;
+
use radicle_types::traits::release_mut::ReleasesMut;
+
use radicle_types::IrohState;
+

+
use crate::AppState;
+

+
#[tauri::command]
+
pub async fn list_releases(
+
    ctx: tauri::State<'_, AppState>,
+
    iroh: tauri::State<'_, IrohState>,
+
    rid: RepoId,
+
) -> Result<Vec<release::Release>, Error> {
+
    let our_did = radicle::identity::Did::from(ctx.profile.public_key);
+
    let endpoint_id = iroh.iroh_router.endpoint().id().to_string();
+
    let ctx = ctx.inner().clone();
+
    let mut releases =
+
        tauri::async_runtime::spawn_blocking(move || ctx.list_releases(rid)).await??;
+
    for release in &mut releases {
+
        release::set_endpoint_flags(release, &our_did, &endpoint_id);
+
    }
+
    Ok(releases)
+
}
+

+
#[tauri::command]
+
pub async fn release_by_id(
+
    ctx: tauri::State<'_, AppState>,
+
    iroh: tauri::State<'_, IrohState>,
+
    rid: RepoId,
+
    release_id: String,
+
) -> Result<Option<release::Release>, Error> {
+
    let our_did = radicle::identity::Did::from(ctx.profile.public_key);
+
    let endpoint_id = iroh.iroh_router.endpoint().id().to_string();
+
    let ctx = ctx.inner().clone();
+
    let mut release =
+
        tauri::async_runtime::spawn_blocking(move || ctx.release_by_id(rid, release_id)).await??;
+
    if let Some(r) = release.as_mut() {
+
        release::set_endpoint_flags(r, &our_did, &endpoint_id);
+
    }
+
    Ok(release)
+
}
+

+
#[tauri::command]
+
pub async fn releases_by_commit(
+
    ctx: tauri::State<'_, AppState>,
+
    iroh: tauri::State<'_, IrohState>,
+
    rid: RepoId,
+
    sha: git::Oid,
+
) -> Result<Vec<release::Release>, Error> {
+
    let our_did = radicle::identity::Did::from(ctx.profile.public_key);
+
    let endpoint_id = iroh.iroh_router.endpoint().id().to_string();
+
    let ctx = ctx.inner().clone();
+
    let mut releases =
+
        tauri::async_runtime::spawn_blocking(move || ctx.releases_by_commit(rid, sha)).await??;
+
    for release in &mut releases {
+
        release::set_endpoint_flags(release, &our_did, &endpoint_id);
+
    }
+
    Ok(releases)
+
}
+

+
/// CID computation can be expensive on large files; off-load to the
+
/// blocking pool so the IPC thread stays responsive.
+
#[tauri::command]
+
pub async fn compute_artifact_cid(
+
    ctx: tauri::State<'_, AppState>,
+
    path: PathBuf,
+
) -> Result<String, Error> {
+
    let ctx = ctx.inner().clone();
+
    tauri::async_runtime::spawn_blocking(move || ctx.compute_cid(path)).await?
+
}
+

+
#[tauri::command]
+
pub async fn create_or_open_release(
+
    ctx: tauri::State<'_, AppState>,
+
    rid: RepoId,
+
    oid: git::Oid,
+
    tag: Option<git::Oid>,
+
) -> Result<String, Error> {
+
    let ctx = ctx.inner().clone();
+
    tauri::async_runtime::spawn_blocking(move || ctx.create_or_open_release(rid, oid, tag)).await?
+
}
+

+
#[tauri::command]
+
pub async fn add_artifact(
+
    ctx: tauri::State<'_, AppState>,
+
    rid: RepoId,
+
    release_id: String,
+
    cid: String,
+
    name: String,
+
) -> Result<(), Error> {
+
    let ctx = ctx.inner().clone();
+
    tauri::async_runtime::spawn_blocking(move || ctx.add_artifact(rid, release_id, cid, name))
+
        .await?
+
}
+

+
#[tauri::command]
+
pub async fn add_location(
+
    ctx: tauri::State<'_, AppState>,
+
    rid: RepoId,
+
    release_id: String,
+
    cid: String,
+
    url: String,
+
) -> Result<(), Error> {
+
    let ctx = ctx.inner().clone();
+
    tauri::async_runtime::spawn_blocking(move || ctx.add_location(rid, release_id, cid, url))
+
        .await?
+
}
+

+
#[tauri::command]
+
pub async fn remove_location(
+
    ctx: tauri::State<'_, AppState>,
+
    rid: RepoId,
+
    release_id: String,
+
    cid: String,
+
    url: String,
+
) -> Result<(), Error> {
+
    let ctx = ctx.inner().clone();
+
    tauri::async_runtime::spawn_blocking(move || ctx.remove_location(rid, release_id, cid, url))
+
        .await?
+
}
+

+
#[tauri::command]
+
pub async fn attest_artifact(
+
    ctx: tauri::State<'_, AppState>,
+
    rid: RepoId,
+
    release_id: String,
+
    cid: String,
+
) -> Result<(), Error> {
+
    let ctx = ctx.inner().clone();
+
    tauri::async_runtime::spawn_blocking(move || ctx.attest_artifact(rid, release_id, cid)).await?
+
}
+

+
#[tauri::command]
+
pub async fn set_metadata(
+
    ctx: tauri::State<'_, AppState>,
+
    rid: RepoId,
+
    release_id: String,
+
    cid: String,
+
    key: String,
+
    value: serde_json::Value,
+
) -> Result<(), Error> {
+
    let ctx = ctx.inner().clone();
+
    tauri::async_runtime::spawn_blocking(move || ctx.set_metadata(rid, release_id, cid, key, value))
+
        .await?
+
}
+

+
#[tauri::command]
+
pub async fn remove_metadata(
+
    ctx: tauri::State<'_, AppState>,
+
    rid: RepoId,
+
    release_id: String,
+
    cid: String,
+
    key: String,
+
) -> Result<(), Error> {
+
    let ctx = ctx.inner().clone();
+
    tauri::async_runtime::spawn_blocking(move || ctx.remove_metadata(rid, release_id, cid, key))
+
        .await?
+
}
+

+
#[tauri::command]
+
pub async fn redact_artifact(
+
    ctx: tauri::State<'_, AppState>,
+
    rid: RepoId,
+
    release_id: String,
+
    cid: String,
+
    reason: String,
+
) -> Result<(), Error> {
+
    let ctx = ctx.inner().clone();
+
    tauri::async_runtime::spawn_blocking(move || ctx.redact_artifact(rid, release_id, cid, reason))
+
        .await?
+
}
+

+
// Seed / unseed -------------------------------------------------------------
+

+
#[tauri::command]
+
pub async fn seed_artifact(
+
    ctx: tauri::State<'_, AppState>,
+
    iroh: tauri::State<'_, IrohState>,
+
    rid: RepoId,
+
    release_id: String,
+
    cid: String,
+
    source_path: PathBuf,
+
) -> Result<String, Error> {
+
    // 1. Import bytes into the store with ImportMode::Copy so the source
+
    //    file/dir can be moved or deleted later without breaking the seed.
+
    seeder::seed(&iroh.blobs, &cid, &source_path).await?;
+

+
    // 2. Register the location on the COB so peers can discover us. Form
+
    //    is iroh://{our_endpoint_id} — explicit because our iroh key is
+
    //    independent from the Radicle DID.
+
    let url = seeder::our_iroh_url(iroh.iroh_router.endpoint());
+
    let url_for_return = url.clone();
+
    let ctx_clone = ctx.inner().clone();
+
    tauri::async_runtime::spawn_blocking(move || ctx_clone.add_location(rid, release_id, cid, url))
+
        .await??;
+

+
    Ok(url_for_return)
+
}
+

+
#[tauri::command]
+
pub async fn unseed_artifact(
+
    ctx: tauri::State<'_, AppState>,
+
    iroh: tauri::State<'_, IrohState>,
+
    rid: RepoId,
+
    release_id: String,
+
    cid: String,
+
) -> Result<(), Error> {
+
    // Drop the COB location first so peers stop trying to reach us before
+
    // we untag the blob.
+
    let url = seeder::our_iroh_url(iroh.iroh_router.endpoint());
+
    let cid_for_remove = cid.clone();
+
    let release_id_for_remove = release_id.clone();
+
    let ctx_clone = ctx.inner().clone();
+
    tauri::async_runtime::spawn_blocking(move || {
+
        ctx_clone.remove_location(rid, release_id_for_remove, cid_for_remove, url)
+
    })
+
    .await??;
+

+
    seeder::unseed(&iroh.blobs, &cid).await?;
+
    Ok(())
+
}
+

+
#[tauri::command]
+
pub async fn is_seeding(iroh: tauri::State<'_, IrohState>, cid: String) -> Result<bool, Error> {
+
    seeder::is_seeded_str(&iroh.blobs, &cid).await
+
}
+

+
/// List every artifact we are currently seeding, across all repos, with
+
/// a disk-usage estimate per entry. Used by the global seeding settings
+
/// view so the user can audit (and prune) what they're sharing.
+
#[tauri::command]
+
pub async fn list_seeded_artifacts(
+
    ctx: tauri::State<'_, AppState>,
+
    iroh: tauri::State<'_, IrohState>,
+
) -> Result<Vec<release::SeededArtifact>, Error> {
+
    let cids = seeder::seeded_cids(&iroh.blobs).await?;
+
    let cid_strings: std::collections::HashSet<String> =
+
        cids.iter().map(|c| c.to_string()).collect();
+

+
    let ctx_clone = ctx.inner().clone();
+
    let mut entries =
+
        tauri::async_runtime::spawn_blocking(move || ctx_clone.find_seeded_artifacts(&cid_strings))
+
            .await??;
+

+
    // Sizes are filled in here, on the async runtime, because the iroh
+
    // Store is async-only and the trait method runs on the blocking pool.
+
    for entry in &mut entries {
+
        entry.size_bytes = seeder::artifact_size_str(&iroh.blobs, &entry.cid).await;
+
    }
+
    Ok(entries)
+
}
+

+
// Download ------------------------------------------------------------------
+

+
use radicle_types::fetch;
+
use tauri::Emitter;
+

+
/// Fetch an artifact from its COB locations and write it to `dest`. For
+
/// blobs that have HTTP/HTTPS locations, HTTP is tried first because it
+
/// is fast and typically succeeds without NAT traversal; iroh is used
+
/// when HTTP fails or no HTTP URLs are listed. Collections always go
+
/// through iroh (radicle-artifact does not implement HTTP collection
+
/// fetch). Streams `artifact_progress` events keyed by `cid` so the UI
+
/// can render a progress bar per row.
+
#[tauri::command]
+
pub async fn download_artifact(
+
    app: tauri::AppHandle,
+
    ctx: tauri::State<'_, AppState>,
+
    iroh: tauri::State<'_, IrohState>,
+
    rid: RepoId,
+
    release_id: String,
+
    cid: String,
+
    dest: PathBuf,
+
) -> Result<(), Error> {
+
    // Pull the (did, urls) snapshot synchronously so we can hold no COB
+
    // locks during the network call.
+
    let cid_for_locations = cid.clone();
+
    let release_id_for_locations = release_id.clone();
+
    let ctx_clone = ctx.inner().clone();
+
    let locations = tauri::async_runtime::spawn_blocking(move || {
+
        ctx_clone.artifact_locations(rid, release_id_for_locations, cid_for_locations)
+
    })
+
    .await??;
+

+
    let (parsed_cid, hash, kind) = fetch::cid_to_hash_str(&cid)?;
+

+
    let emit = |stage: &str, bytes: Option<u64>| {
+
        let mut payload = serde_json::json!({ "cid": cid, "stage": stage });
+
        if let Some(b) = bytes {
+
            payload["bytes"] = serde_json::json!(b);
+
        }
+
        let _ = app.emit("artifact_progress", payload);
+
    };
+

+
    // Fast path: bytes already in the local iroh store. Skip every
+
    // network transport and just write them straight to disk.
+
    if iroh.blobs.blobs().has(hash).await.unwrap_or(false) {
+
        emit("writing", None);
+
        fetch::export(&iroh.blobs, hash, kind, &dest).await?;
+
        emit("done", None);
+
        return Ok(());
+
    }
+

+
    emit("connecting", None);
+

+
    let mut errors: Vec<String> = Vec::new();
+
    let http_urls = fetch::http_urls(&locations);
+
    let try_http_first = !http_urls.is_empty() && matches!(kind, fetch::ArtifactKind::Blob);
+

+
    if try_http_first {
+
        for url in &http_urls {
+
            let url_clone = url.clone();
+
            let cid_clone = parsed_cid;
+
            let dest_clone = dest.clone();
+
            match tauri::async_runtime::spawn_blocking(move || {
+
                fetch::fetch_http_blob(&url_clone, &cid_clone, &dest_clone)
+
            })
+
            .await?
+
            {
+
                Ok(()) => {
+
                    emit("done", None);
+
                    return Ok(());
+
                }
+
                Err(e) => errors.push(format!("{url}: {e}")),
+
            }
+
        }
+
    }
+

+
    // Either no HTTP URLs, or every HTTP attempt failed — fall through to
+
    // the iroh path, which loads bytes into the persistent store and then
+
    // exports them to the destination.
+
    let cid_for_progress = cid.clone();
+
    let app_for_progress = app.clone();
+
    let iroh_result = fetch::fetch_artifact(
+
        &iroh.blobs,
+
        iroh.iroh_router.endpoint(),
+
        &parsed_cid,
+
        &locations,
+
        |stage| {
+
            let payload = match stage {
+
                fetch::FetchStage::Connecting => {
+
                    serde_json::json!({ "cid": cid_for_progress, "stage": "connecting" })
+
                }
+
                fetch::FetchStage::Downloading { bytes } => serde_json::json!({
+
                    "cid": cid_for_progress,
+
                    "stage": "downloading",
+
                    "bytes": bytes,
+
                }),
+
            };
+
            let _ = app_for_progress.emit("artifact_progress", payload);
+
        },
+
    )
+
    .await;
+

+
    match iroh_result {
+
        Ok(()) => {
+
            emit("writing", None);
+
            fetch::export(&iroh.blobs, hash, kind, &dest).await?;
+
            emit("done", None);
+
            Ok(())
+
        }
+
        Err(e) => {
+
            errors.push(format!("iroh: {e}"));
+
            // If HTTP wasn't even an option and iroh failed, surface the
+
            // raw iroh error so the user sees something actionable.
+
            if errors.len() == 1 {
+
                return Err(e);
+
            }
+
            Err(Error::AllTransportsFailed {
+
                cid: cid.clone(),
+
                reasons: errors.join("; "),
+
            })
+
        }
+
    }
+
}
+

+
// File picker ---------------------------------------------------------------
+

+
use tauri_plugin_dialog::DialogExt;
+

+
/// Open a multi-file picker. Returns the selected paths as strings, or an
+
/// empty Vec if the user cancelled. The frontend feeds each path into
+
/// `compute_artifact_cid` then `add_artifact`.
+
#[tauri::command]
+
pub async fn pick_artifact_files(app: tauri::AppHandle) -> Result<Vec<String>, Error> {
+
    let (tx, rx) = tokio::sync::oneshot::channel();
+
    app.dialog().file().pick_files(move |paths| {
+
        let _ = tx.send(paths.unwrap_or_default());
+
    });
+
    let paths = rx.await.map_err(|_| Error::DialogClosed)?;
+
    Ok(paths
+
        .into_iter()
+
        .filter_map(|p| p.into_path().ok())
+
        .map(|p| p.to_string_lossy().into_owned())
+
        .collect())
+
}
+

+
/// Open a single-directory picker. Used when the user wants to attach a
+
/// directory artifact (which becomes a Collection CID).
+
#[tauri::command]
+
pub async fn pick_artifact_directory(app: tauri::AppHandle) -> Result<Option<String>, Error> {
+
    let (tx, rx) = tokio::sync::oneshot::channel();
+
    app.dialog().file().pick_folder(move |path| {
+
        let _ = tx.send(path);
+
    });
+
    let path = rx.await.map_err(|_| Error::DialogClosed)?;
+
    Ok(path
+
        .and_then(|p| p.into_path().ok())
+
        .map(|p| p.to_string_lossy().into_owned()))
+
}
+

+
/// Open the OS "Save as" dialog seeded with `suggested_name`. Returns the
+
/// path the user picked, or `None` if they cancelled. The frontend feeds
+
/// the path into `download_artifact` as the destination for a Blob fetch.
+
#[tauri::command]
+
pub async fn pick_artifact_save_path(
+
    app: tauri::AppHandle,
+
    suggested_name: String,
+
) -> Result<Option<String>, Error> {
+
    let (tx, rx) = tokio::sync::oneshot::channel();
+
    app.dialog()
+
        .file()
+
        .set_file_name(suggested_name)
+
        .save_file(move |path| {
+
            let _ = tx.send(path);
+
        });
+
    let path = rx.await.map_err(|_| Error::DialogClosed)?;
+
    Ok(path
+
        .and_then(|p| p.into_path().ok())
+
        .map(|p| p.to_string_lossy().into_owned()))
+
}
+

+
// Settings ------------------------------------------------------------------
+

+
#[tauri::command]
+
pub fn get_auto_seed_artifacts(ctx: tauri::State<AppState>) -> Result<bool, Error> {
+
    let settings = radicle_types::settings::load(ctx.profile.home().path());
+
    Ok(settings.auto_seed_artifacts)
+
}
+

+
#[tauri::command]
+
pub fn set_auto_seed_artifacts(ctx: tauri::State<AppState>, enabled: bool) -> Result<(), Error> {
+
    let mut settings = radicle_types::settings::load(ctx.profile.home().path());
+
    settings.auto_seed_artifacts = enabled;
+
    radicle_types::settings::save(ctx.profile.home().path(), &settings)
+
}
modified crates/radicle-tauri/src/commands/repo.rs
@@ -122,6 +122,15 @@ pub fn repo_commit(
}

#[tauri::command]
+
pub async fn list_tags(
+
    ctx: tauri::State<'_, AppState>,
+
    rid: RepoId,
+
) -> Result<Vec<types::repo::Tag>, Error> {
+
    let ctx = ctx.inner().clone();
+
    tauri::async_runtime::spawn_blocking(move || ctx.list_tags(rid)).await?
+
}
+

+
#[tauri::command]
pub fn seed(ctx: tauri::State<AppState>, rid: RepoId) -> Result<(), Error> {
    ctx.seed(rid)
}
modified crates/radicle-tauri/src/commands/startup.rs
@@ -5,8 +5,9 @@ use radicle::node::{Handle, Node, NOTIFICATIONS_DB_FILE};

use radicle_types::config::{Config, Version};
use radicle_types::error::Error;
+
use radicle_types::seeder;
use radicle_types::traits::Profile;
-
use radicle_types::{domain, AppState};
+
use radicle_types::{domain, AppState, IrohState};

#[tauri::command]
pub(crate) fn version(app: AppHandle) -> Result<Version, Error> {
@@ -67,7 +68,7 @@ pub(crate) fn check_radicle_cli(ctx: tauri::State<AppState>) -> Result<(), Error
}

#[tauri::command]
-
pub(crate) fn startup(app: AppHandle) -> Result<Config, Error> {
+
pub(crate) async fn startup(app: AppHandle) -> Result<Config, Error> {
    let profile = radicle::Profile::load()?;
    let home = profile.home();

@@ -83,8 +84,11 @@ pub(crate) fn startup(app: AppHandle) -> Result<Config, Error> {
    let inbox_service = domain::inbox::service::Service::new(inbox_db);
    let patch_service = domain::patch::service::Service::new(cob_db);

-
    let node_handle = app.app_handle().clone();
+
    // Bring iroh up before the first frontend command. Anything previously
+
    // tagged in the FsStore is served the moment the router is up.
+
    let seeder = seeder::bootstrap(home.path()).await?;

+
    let node_handle = app.app_handle().clone();
    let node = Node::new(profile.home().socket_from_env());

    app.manage(inbox_service);
@@ -99,6 +103,10 @@ pub(crate) fn startup(app: AppHandle) -> Result<Config, Error> {

    let state = AppState { profile };
    app.manage(state.clone());
+
    app.manage(IrohState {
+
        blobs: seeder.blobs,
+
        iroh_router: seeder.router,
+
    });

    Ok(state.config())
}
modified crates/radicle-tauri/src/lib.rs
@@ -1,6 +1,7 @@
mod commands;

-
use radicle_types::AppState;
+
use radicle_types::{AppState, IrohState};
+
use tauri::Manager;

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

@@ -43,6 +44,28 @@ pub fn run() {
            cob::patch::revisions_by_patch,
            cob::patch::revision_by_patch_and_id,
            cob::patch::revisions_by_patch,
+
            cob::release::list_releases,
+
            cob::release::release_by_id,
+
            cob::release::releases_by_commit,
+
            cob::release::compute_artifact_cid,
+
            cob::release::create_or_open_release,
+
            cob::release::add_artifact,
+
            cob::release::add_location,
+
            cob::release::remove_location,
+
            cob::release::attest_artifact,
+
            cob::release::set_metadata,
+
            cob::release::remove_metadata,
+
            cob::release::redact_artifact,
+
            cob::release::seed_artifact,
+
            cob::release::unseed_artifact,
+
            cob::release::is_seeding,
+
            cob::release::list_seeded_artifacts,
+
            cob::release::download_artifact,
+
            cob::release::pick_artifact_files,
+
            cob::release::pick_artifact_directory,
+
            cob::release::pick_artifact_save_path,
+
            cob::release::get_auto_seed_artifacts,
+
            cob::release::set_auto_seed_artifacts,
            cob::save_embed_by_bytes,
            cob::save_embed_by_clipboard,
            cob::save_embed_by_path,
@@ -57,6 +80,7 @@ pub fn run() {
            repo::diff_stats,
            repo::list_commits,
            repo::list_repo_commits,
+
            repo::list_tags,
            repo::list_repos,
            repo::list_repos_summary,
            repo::repo_by_id,
@@ -75,6 +99,19 @@ pub fn run() {
            thread::create_issue_comment,
            thread::create_patch_comment,
        ])
-
        .run(tauri::generate_context!())
-
        .expect("error while running tauri application");
+
        .build(tauri::generate_context!())
+
        .expect("error while building tauri application")
+
        .run(|app, event| {
+
            // Shut down the iroh router cleanly so peers see us go away
+
            // instead of timing out.
+
            if let tauri::RunEvent::Exit = event {
+
                if let Some(state) = app.try_state::<IrohState>() {
+
                    log::info!("Shutting down iroh router");
+
                    let router = state.iroh_router.clone();
+
                    tauri::async_runtime::block_on(async move {
+
                        let _ = router.shutdown().await;
+
                    });
+
                }
+
            }
+
        });
}
modified crates/radicle-types/Cargo.toml
@@ -15,9 +15,15 @@ log = { version = "0.4.22" }
infer = { version = "0.19.0" }
mime-infer = { version = "3.0.0" }
radicle = { version = "0.24" }
+
radicle-artifact = { version = "0.13", features = ["share"] }
radicle-job = { version = "0.6" }
radicle-localtime = { version = "0.1.0", features = ["serde"] }
radicle-surf = { version = "0.27.1", features = ["serde"] }
+
iroh = { version = "1.0.0-rc.0" }
+
iroh-blobs = { version = "0.101", features = ["fs-store"] }
+
cid = { version = "0.11" }
+
ureq = { version = "3.3.0" }
+
futures-lite = { version = "2" }
serde = { version = "1.0.0", features = ["derive"] }
serde_json = { version = "1.0.0" }
sqlite = { version = "0.37.0", features = ["bundled"] }
added crates/radicle-types/bindings/cob/release/Artifact.ts
@@ -0,0 +1,33 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../Author";
+
import type { ArtifactKind } from "./ArtifactKind";
+
import type { Location } from "./Location";
+
import type { Redaction } from "./Redaction";
+

+
export type Artifact = {
+
  cid: string;
+
  name: string;
+
  kind: ArtifactKind;
+
  author: Author;
+
  locations: Array<Location>;
+
  attestations: Array<Author>;
+
  redactions: Array<Redaction>;
+
  /**
+
   * True when this device's iroh endpoint id appears as the host of
+
   * one of the location URLs we (the local DID) wrote — i.e. we are
+
   * actively advertising the artifact from the running process.
+
   */
+
  sharedFromHere: boolean;
+
  /**
+
   * True when our DID has at least one `iroh://` URL on the COB whose
+
   * host is *not* this device's iroh endpoint. Usually means we (or a
+
   * past install on a different machine, e.g. the CLI) advertised this
+
   * artifact from somewhere else. The bytes are not necessarily local.
+
   */
+
  sharedFromOther: boolean;
+
  /**
+
   * Free-form key/value annotations contributed by the artifact author
+
   * or repo delegates. Authorization is enforced upstream.
+
   */
+
  metadata?: Record<string, unknown>;
+
};
added crates/radicle-types/bindings/cob/release/ArtifactKind.ts
@@ -0,0 +1,3 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
export type ArtifactKind = "blob" | "collection" | "unknown";
added crates/radicle-types/bindings/cob/release/Location.ts
@@ -0,0 +1,4 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../Author";
+

+
export type Location = { peer: Author; urls: Array<string> };
added crates/radicle-types/bindings/cob/release/Redaction.ts
@@ -0,0 +1,4 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../Author";
+

+
export type Redaction = { peer: Author; reason: string };
added crates/radicle-types/bindings/cob/release/Release.ts
@@ -0,0 +1,30 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../Author";
+
import type { Artifact } from "./Artifact";
+

+
export type Release = {
+
  id: string;
+
  oid: string;
+
  /**
+
   * OID of the annotated tag that was recorded alongside the commit,
+
   * if any. `None` means the release was created from a bare commit.
+
   */
+
  tag?: string;
+
  /**
+
   * Refname of the tag pointed at by `tag`, resolved on the server so
+
   * the UI doesn't need to fan out a separate `list_tags` lookup per
+
   * release. Omitted when the tag has been deleted locally.
+
   */
+
  tagName?: string;
+
  /**
+
   * DID of the user who authored this release COB.
+
   */
+
  creator: Author;
+
  /**
+
   * Subject line of the released commit, resolved on the server so the
+
   * list view can render it without an extra round-trip per row.
+
   */
+
  commitSummary?: string;
+
  timestamp: number;
+
  artifacts: Array<Artifact>;
+
};
added crates/radicle-types/bindings/cob/release/SeededArtifact.ts
@@ -0,0 +1,25 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { ArtifactKind } from "./ArtifactKind";
+

+
/**
+
 * Single row in the global "what am I seeding?" list. Carries enough
+
 * context for the UI to navigate back to the originating release and to
+
 * call `unseed_artifact` without an extra lookup.
+
 */
+
export type SeededArtifact = {
+
  rid: string;
+
  /**
+
   * Project name from the repo's identity doc, or an empty string when
+
   * the repo has no project payload (rare; surfaces as the RID in UI).
+
   */
+
  repoName: string;
+
  releaseId: string;
+
  cid: string;
+
  name: string;
+
  kind: ArtifactKind;
+
  /**
+
   * Best-effort sum of stored bytes. Returns 0 if the size lookup fails
+
   * rather than failing the entire listing.
+
   */
+
  sizeBytes: number;
+
};
added crates/radicle-types/bindings/repo/Tag.ts
@@ -0,0 +1,22 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
export type Tag = {
+
  /**
+
   * Short tag refname, e.g. `v1.0.0`.
+
   */
+
  name: string;
+
  /**
+
   * Tag OID for annotated tags, commit OID for lightweight tags.
+
   * This is the OID stored on the artifact COB's `tag` field for
+
   * annotated tags.
+
   */
+
  oid: string;
+
  /**
+
   * The commit this tag points at. For lightweight tags this equals
+
   * `oid`; for annotated tags it's the commit reachable via the tag
+
   * object's target.
+
   */
+
  commit: string;
+
  annotated: boolean;
+
  message?: string;
+
};
modified crates/radicle-types/src/cobs.rs
@@ -11,6 +11,7 @@ use radicle::node::{Alias, AliasStore};
pub mod diff;
pub mod issue;
pub mod job;
+
pub mod release;
pub mod repo;
pub mod stream;
pub mod thread;
added crates/radicle-types/src/cobs/release.rs
@@ -0,0 +1,241 @@
+
use std::collections::BTreeMap;
+

+
use radicle::identity::Did;
+
use radicle::node::AliasStore;
+
use radicle_artifact::share::cid_utils;
+
use serde::Serialize;
+
use ts_rs::TS;
+

+
use crate::cobs;
+

+
#[derive(Clone, Serialize, TS, Debug)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/release/")]
+
pub struct Release {
+
    #[ts(as = "String")]
+
    pub id: radicle_artifact::ReleaseId,
+
    #[ts(as = "String")]
+
    pub oid: radicle::git::Oid,
+
    /// OID of the annotated tag that was recorded alongside the commit,
+
    /// if any. `None` means the release was created from a bare commit.
+
    #[serde(skip_serializing_if = "Option::is_none")]
+
    #[ts(as = "Option<String>", optional)]
+
    pub tag: Option<radicle::git::Oid>,
+
    /// Refname of the tag pointed at by `tag`, resolved on the server so
+
    /// the UI doesn't need to fan out a separate `list_tags` lookup per
+
    /// release. Omitted when the tag has been deleted locally.
+
    #[serde(skip_serializing_if = "Option::is_none")]
+
    #[ts(optional)]
+
    pub tag_name: Option<String>,
+
    /// DID of the user who authored this release COB.
+
    pub creator: cobs::Author,
+
    /// Subject line of the released commit, resolved on the server so the
+
    /// list view can render it without an extra round-trip per row.
+
    #[serde(skip_serializing_if = "Option::is_none")]
+
    #[ts(optional)]
+
    pub commit_summary: Option<String>,
+
    #[ts(type = "number")]
+
    pub timestamp: u64,
+
    pub artifacts: Vec<Artifact>,
+
}
+

+
#[derive(Clone, Serialize, TS, Debug)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/release/")]
+
pub struct Artifact {
+
    pub cid: String,
+
    pub name: String,
+
    pub kind: ArtifactKind,
+
    pub author: cobs::Author,
+
    pub locations: Vec<Location>,
+
    pub attestations: Vec<cobs::Author>,
+
    pub redactions: Vec<Redaction>,
+
    /// True when this device's iroh endpoint id appears as the host of
+
    /// one of the location URLs we (the local DID) wrote — i.e. we are
+
    /// actively advertising the artifact from the running process.
+
    pub shared_from_here: bool,
+
    /// True when our DID has at least one `iroh://` URL on the COB whose
+
    /// host is *not* this device's iroh endpoint. Usually means we (or a
+
    /// past install on a different machine, e.g. the CLI) advertised this
+
    /// artifact from somewhere else. The bytes are not necessarily local.
+
    pub shared_from_other: bool,
+
    /// Free-form key/value annotations contributed by the artifact author
+
    /// or repo delegates. Authorization is enforced upstream.
+
    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
+
    #[ts(type = "Record<string, unknown>", optional)]
+
    pub metadata: BTreeMap<String, serde_json::Value>,
+
}
+

+
#[derive(Clone, Serialize, TS, Debug)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/release/")]
+
pub enum ArtifactKind {
+
    Blob,
+
    Collection,
+
    Unknown,
+
}
+

+
/// Single row in the global "what am I seeding?" list. Carries enough
+
/// context for the UI to navigate back to the originating release and to
+
/// call `unseed_artifact` without an extra lookup.
+
#[derive(Clone, Serialize, TS, Debug)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/release/")]
+
pub struct SeededArtifact {
+
    #[ts(as = "String")]
+
    pub rid: radicle::identity::RepoId,
+
    /// Project name from the repo's identity doc, or an empty string when
+
    /// the repo has no project payload (rare; surfaces as the RID in UI).
+
    pub repo_name: String,
+
    pub release_id: String,
+
    pub cid: String,
+
    pub name: String,
+
    pub kind: ArtifactKind,
+
    /// Best-effort sum of stored bytes. Returns 0 if the size lookup fails
+
    /// rather than failing the entire listing.
+
    #[ts(type = "number")]
+
    pub size_bytes: u64,
+
}
+

+
#[derive(Clone, Serialize, TS, Debug)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/release/")]
+
pub struct Location {
+
    pub peer: cobs::Author,
+
    pub urls: Vec<String>,
+
}
+

+
#[derive(Clone, Serialize, TS, Debug)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/release/")]
+
pub struct Redaction {
+
    pub peer: cobs::Author,
+
    pub reason: String,
+
}
+

+
impl Release {
+
    pub fn new(
+
        id: radicle_artifact::ReleaseId,
+
        release: &radicle_artifact::Release,
+
        aliases: &impl AliasStore,
+
        tag_name: Option<String>,
+
        commit_summary: Option<String>,
+
    ) -> Self {
+
        let artifacts = release
+
            .artifacts()
+
            .iter()
+
            .map(|(cid, artifact)| Artifact::new(cid, artifact, aliases))
+
            .collect();
+

+
        Self {
+
            id,
+
            oid: *release.oid(),
+
            tag: release.tag().copied(),
+
            tag_name,
+
            creator: cobs::Author::new(release.creator(), aliases),
+
            commit_summary,
+
            timestamp: release.timestamp(),
+
            artifacts,
+
        }
+
    }
+
}
+

+
impl Artifact {
+
    pub fn new(
+
        cid: &cid::Cid,
+
        artifact: &radicle_artifact::Artifact,
+
        aliases: &impl AliasStore,
+
    ) -> Self {
+
        let kind = match cid_utils::artifact_kind(cid) {
+
            Ok(cid_utils::ArtifactKind::Blob) => ArtifactKind::Blob,
+
            Ok(cid_utils::ArtifactKind::Collection) => ArtifactKind::Collection,
+
            Err(_) => ArtifactKind::Unknown,
+
        };
+

+
        let locations = artifact
+
            .locations()
+
            .iter()
+
            .map(|(did, urls)| Location {
+
                peer: cobs::Author::new(did, aliases),
+
                urls: urls.iter().map(|u| u.to_string()).collect(),
+
            })
+
            .collect();
+

+
        let attestations = artifact
+
            .attestations()
+
            .iter()
+
            .map(|did| cobs::Author::new(did, aliases))
+
            .collect();
+

+
        let redactions = artifact
+
            .redactions()
+
            .iter()
+
            .map(|(did, reason)| Redaction {
+
                peer: cobs::Author::new(did, aliases),
+
                reason: reason.clone(),
+
            })
+
            .collect();
+

+
        // We don't yet know which iroh endpoint id is "us-here" — that
+
        // lives in the iroh state, not in this crate. Default both flags
+
        // to false and let `set_endpoint_flags` refine them at the
+
        // driver layer when an endpoint id is available.
+
        Self {
+
            cid: cid.to_string(),
+
            name: artifact.name().to_string(),
+
            kind,
+
            author: cobs::Author::new(artifact.author(), aliases),
+
            locations,
+
            attestations,
+
            redactions,
+
            shared_from_here: false,
+
            shared_from_other: false,
+
            metadata: artifact.metadata().clone(),
+
        }
+
    }
+
}
+

+
/// Classify the `iroh://` URLs under our DID into `shared_from_here`
+
/// (host matches `endpoint_id`) and `shared_from_other` (any other host
+
/// or a host-less URL). Drivers that have an iroh endpoint call this on
+
/// every release they hand to the UI so the flags reflect *this*
+
/// process, not just "we wrote an iroh URL once."
+
pub fn set_endpoint_flags(release: &mut Release, our_did: &Did, endpoint_id: &str) {
+
    let our_did_str = our_did.to_string();
+
    for artifact in &mut release.artifacts {
+
        let mut here = false;
+
        let mut other = false;
+
        for loc in &artifact.locations {
+
            if loc.peer.did().to_string() != our_did_str {
+
                continue;
+
            }
+
            for url in &loc.urls {
+
                if !url.starts_with("iroh://") {
+
                    continue;
+
                }
+
                // `iroh://<endpoint>` — strip the scheme to compare hosts.
+
                let host = url.trim_start_matches("iroh://");
+
                // Trailing slash from url::Url::to_string is tolerated.
+
                let host = host.trim_end_matches('/');
+
                if host == endpoint_id {
+
                    here = true;
+
                } else {
+
                    other = true;
+
                }
+
            }
+
        }
+
        artifact.shared_from_here = here;
+
        artifact.shared_from_other = other;
+
    }
+
}
+

+
// Quiet the dead-code warning: this map alias keeps the upstream type names
+
// aligned with the DTO at a glance, and may be used later by trait helpers.
+
#[allow(dead_code)]
+
pub(crate) type LocationMap = BTreeMap<Did, std::collections::BTreeSet<url::Url>>;
modified crates/radicle-types/src/error.rs
@@ -167,6 +167,68 @@ pub enum Error {
    /// Serde JSON error.
    #[error(transparent)]
    SerdeJSON(#[from] serde_json::error::Error),
+

+
    /// Iroh / iroh-blobs error.
+
    #[error("iroh: {0}")]
+
    Iroh(String),
+

+
    /// Release creation error.
+
    #[error(transparent)]
+
    ReleaseCreate(#[from] radicle_artifact::error::Create),
+

+
    /// Release redaction error.
+
    #[error(transparent)]
+
    ReleaseRedact(#[from] radicle_artifact::error::Redact),
+

+
    /// Artifact share error (CID parsing, hashing, content addressing).
+
    #[error(transparent)]
+
    ArtifactShare(#[from] radicle_artifact::share::Error),
+

+
    /// CID parse error.
+
    #[error(transparent)]
+
    Cid(#[from] cid::Error),
+

+
    /// URL parse error.
+
    #[error(transparent)]
+
    Url(#[from] url::ParseError),
+

+
    /// COB object id parse error.
+
    #[error(transparent)]
+
    ParseObjectId(#[from] radicle::cob::object::ParseObjectId),
+

+
    /// File picker / dialog closed before returning a result.
+
    #[error("dialog was closed before returning a result")]
+
    DialogClosed,
+

+
    /// No iroh providers reachable for the requested CID.
+
    #[error("no iroh providers reachable for {cid}")]
+
    NoIrohProviders { cid: String },
+

+
    /// The artifact has no usable locations of any supported scheme.
+
    #[error("no locations available for {cid}")]
+
    NoLocations { cid: String },
+

+
    /// All transport attempts (iroh + HTTP) failed for the requested CID.
+
    /// The aggregated messages from each attempt are joined into one string
+
    /// for surfacing in the UI.
+
    #[error("all transports failed for {cid}: {reasons}")]
+
    AllTransportsFailed { cid: String, reasons: String },
+

+
    /// Release with the given id was not found in the COB store.
+
    #[error("release {release_id} not found")]
+
    ReleaseNotFound { release_id: String },
+

+
    /// Artifact CID is not registered against the given release.
+
    #[error("artifact {cid} not in release {release_id}")]
+
    ArtifactNotInRelease { cid: String, release_id: String },
+

+
    /// Persisted iroh secret key file does not contain 32 bytes.
+
    #[error("malformed iroh key at {path}")]
+
    MalformedIrohKey { path: std::path::PathBuf },
+

+
    /// CID computed from imported content does not match the expected CID.
+
    #[error("cid mismatch: expected {expected}, got {actual}")]
+
    CidMismatch { expected: String, actual: String },
}

impl Error {
@@ -194,6 +256,29 @@ impl Error {
                "AliasError.InvalidAlias"
            }
            Error::FileTooLarge(_) => "PayloadError.TooLarge",
+
            Error::DialogClosed => "DialogError.Closed",
+
            Error::NoIrohProviders { .. } => "ArtifactError.NoProviders",
+
            Error::NoLocations { .. } => "ArtifactError.NoLocations",
+
            Error::AllTransportsFailed { .. } => "ArtifactError.AllTransportsFailed",
+
            Error::ReleaseNotFound { .. } => "ReleaseError.NotFound",
+
            Error::ArtifactNotInRelease { .. } => "ReleaseError.ArtifactNotFound",
+
            Error::CidMismatch { .. } => "ArtifactError.CidMismatch",
+
            Error::MalformedIrohKey { .. } => "IrohError.MalformedKey",
+
            Error::ReleaseRedact(radicle_artifact::error::Redact::NotFound { .. }) => {
+
                "ReleaseError.ArtifactNotFound"
+
            }
+
            Error::ReleaseRedact(radicle_artifact::error::Redact::ReasonTooLong { .. }) => {
+
                "ReleaseError.RedactionReasonTooLong"
+
            }
+
            Error::ReleaseCreate(radicle_artifact::error::Create::MissingTag { .. }) => {
+
                "ReleaseError.MissingTag"
+
            }
+
            Error::ReleaseCreate(radicle_artifact::error::Create::TagMismatch { .. }) => {
+
                "ReleaseError.TagMismatch"
+
            }
+
            Error::ReleaseCreate(radicle_artifact::error::Create::PeelFailed { .. }) => {
+
                "ReleaseError.TagPeelFailed"
+
            }
            _ => "UnknownError",
        }
    }
added crates/radicle-types/src/fetch.rs
@@ -0,0 +1,248 @@
+
use std::collections::{BTreeMap, BTreeSet};
+
use std::path::Path;
+
use std::str::FromStr;
+

+
use cid::Cid;
+
use futures_lite::StreamExt;
+
use iroh::Endpoint;
+
use iroh_blobs::api::downloader::{DownloadProgressItem, Downloader};
+
use iroh_blobs::api::Store;
+
use iroh_blobs::format::collection::Collection;
+
use iroh_blobs::{BlobFormat, Hash, HashAndFormat};
+
use radicle::identity::Did;
+
use radicle_artifact::share::{cid_utils, keys};
+
use url::Url;
+

+
use crate::error::Error;
+

+
// Re-export so downstream crates (radicle-tauri) can branch on artifact
+
// kind without depending on radicle-artifact directly.
+
pub use radicle_artifact::share::cid_utils::ArtifactKind;
+

+
/// Progress stages reported during an in-flight fetch. The frontend uses
+
/// these to render per-CID download status.
+
pub enum FetchStage {
+
    Connecting,
+
    Downloading { bytes: u64 },
+
}
+

+
/// Outcome of an artifact fetch. `IrohStore` means the bytes are now in
+
/// the iroh blob store and the caller needs to `export` them to disk;
+
/// `DiskDirect` means the bytes were written directly to the user's
+
/// destination (HTTP path) and no export step is required.
+
pub enum FetchOutcome {
+
    IrohStore,
+
    DiskDirect,
+
}
+

+
/// Fetch an artifact's bytes into the iroh blob store from any of the
+
/// iroh providers reachable through `locations`. Streams `on_progress`
+
/// events as the download progresses. Bytes are verified against the CID
+
/// via iroh-blobs' built-in hash check. Returns `Err(NoIrohProviders)`
+
/// when none of the locations are iroh URLs — callers handle HTTP
+
/// fallback via [`fetch_http_blob`].
+
pub async fn fetch_artifact<F>(
+
    store: &Store,
+
    endpoint: &Endpoint,
+
    cid: &Cid,
+
    locations: &BTreeMap<Did, BTreeSet<Url>>,
+
    mut on_progress: F,
+
) -> Result<(), Error>
+
where
+
    F: FnMut(FetchStage),
+
{
+
    let kind = cid_utils::artifact_kind(cid)?;
+
    let hash = cid_utils::cid_to_blake3_hash(cid)?;
+
    let format = match kind {
+
        cid_utils::ArtifactKind::Blob => BlobFormat::Raw,
+
        cid_utils::ArtifactKind::Collection => BlobFormat::HashSeq,
+
    };
+

+
    let providers = resolve_iroh_providers(locations);
+
    if providers.is_empty() {
+
        return Err(Error::NoIrohProviders {
+
            cid: cid.to_string(),
+
        });
+
    }
+

+
    let downloader = Downloader::new(store, endpoint);
+
    let mut stream = downloader
+
        .download(HashAndFormat { hash, format }, providers)
+
        .stream()
+
        .await
+
        .map_err(|e| Error::Iroh(format!("download init: {e}")))?;
+

+
    while let Some(item) = stream.next().await {
+
        match item {
+
            DownloadProgressItem::TryProvider { .. } => on_progress(FetchStage::Connecting),
+
            DownloadProgressItem::Progress(bytes) => on_progress(FetchStage::Downloading { bytes }),
+
            DownloadProgressItem::PartComplete { .. }
+
            | DownloadProgressItem::ProviderFailed { .. } => {}
+
            DownloadProgressItem::Error(e) => return Err(Error::Iroh(format!("download: {e}"))),
+
            DownloadProgressItem::DownloadError => {
+
                return Err(Error::Iroh("download failed".into()));
+
            }
+
        }
+
    }
+

+
    Ok(())
+
}
+

+
/// Extract every HTTP/HTTPS URL from `locations`, preserving insertion
+
/// order so the caller can try them in a deterministic sequence.
+
pub fn http_urls(locations: &BTreeMap<Did, BTreeSet<Url>>) -> Vec<Url> {
+
    let mut out = Vec::new();
+
    for urls in locations.values() {
+
        for url in urls {
+
            if matches!(url.scheme(), "http" | "https") {
+
                out.push(url.clone());
+
            }
+
        }
+
    }
+
    out
+
}
+

+
/// Download a blob artifact via HTTP directly to disk. Writes to a
+
/// `.partial` sibling first, verifies the CID against the file contents,
+
/// and only then renames into place — matching the safety pattern in
+
/// `radicle_artifact::share::fetch::download`. Run on the blocking pool;
+
/// `ureq` is synchronous.
+
pub fn fetch_http_blob(url: &Url, expected_cid: &Cid, dest: &Path) -> Result<(), Error> {
+
    use std::io::{BufWriter, Read, Write};
+

+
    let agent = ureq::Agent::new_with_config(
+
        ureq::Agent::config_builder()
+
            .timeout_connect(Some(std::time::Duration::from_secs(10)))
+
            .build(),
+
    );
+
    let mut response = agent
+
        .get(url.as_str())
+
        .call()
+
        .map_err(|e| Error::Iroh(format!("http get {url}: {e}")))?;
+

+
    let partial = dest.with_extension("partial");
+
    if let Some(parent) = partial.parent() {
+
        std::fs::create_dir_all(parent)?;
+
    }
+
    {
+
        let file = std::fs::File::create(&partial)?;
+
        let mut writer = BufWriter::new(file);
+
        let mut buf = [0u8; 64 * 1024];
+
        let mut body = response.body_mut().as_reader();
+
        loop {
+
            let n = body.read(&mut buf).map_err(|e| {
+
                std::fs::remove_file(&partial).ok();
+
                Error::Iroh(format!("http read {url}: {e}"))
+
            })?;
+
            if n == 0 {
+
                break;
+
            }
+
            writer.write_all(&buf[..n]).map_err(|e| {
+
                std::fs::remove_file(&partial).ok();
+
                Error::Io(e)
+
            })?;
+
        }
+
        writer.flush().map_err(|e| {
+
            std::fs::remove_file(&partial).ok();
+
            Error::Io(e)
+
        })?;
+
    }
+

+
    if let Err(e) = cid_utils::verify_cid_file(&partial, expected_cid) {
+
        std::fs::remove_file(&partial).ok();
+
        return Err(Error::Iroh(format!("cid verification: {e}")));
+
    }
+

+
    std::fs::rename(&partial, dest)?;
+
    Ok(())
+
}
+

+
/// Resolve the iroh-blobs `Hash` and content kind for a CID. Useful when
+
/// the caller needs both the format (for fetch / export) and the kind
+
/// (for branching blob vs collection).
+
pub fn cid_to_hash(cid: &Cid) -> Result<(Hash, cid_utils::ArtifactKind), Error> {
+
    let kind = cid_utils::artifact_kind(cid)?;
+
    let hash = cid_utils::cid_to_blake3_hash(cid)?;
+
    Ok((hash, kind))
+
}
+

+
/// String-taking variant for callers (Tauri commands) that don't pull in
+
/// the `cid` crate directly.
+
pub fn cid_to_hash_str(cid: &str) -> Result<(Cid, Hash, cid_utils::ArtifactKind), Error> {
+
    let parsed = Cid::from_str(cid)?;
+
    let (hash, kind) = cid_to_hash(&parsed)?;
+
    Ok((parsed, hash, kind))
+
}
+

+
/// Write a previously-fetched artifact from the store to a user-chosen
+
/// destination on disk. Blobs land at `dest`; collections create `dest`
+
/// as a directory and write each entry under its declared name.
+
pub async fn export(
+
    store: &Store,
+
    hash: Hash,
+
    kind: cid_utils::ArtifactKind,
+
    dest: &Path,
+
) -> Result<(), Error> {
+
    let blobs = store.blobs();
+
    match kind {
+
        cid_utils::ArtifactKind::Blob => {
+
            let bytes = blobs
+
                .get_bytes(hash)
+
                .await
+
                .map_err(|e| Error::Iroh(format!("read blob: {e}")))?;
+
            if let Some(parent) = dest.parent() {
+
                std::fs::create_dir_all(parent)?;
+
            }
+
            std::fs::write(dest, &bytes)?;
+
        }
+
        cid_utils::ArtifactKind::Collection => {
+
            let collection = Collection::load(hash, store)
+
                .await
+
                .map_err(|e| Error::Iroh(format!("load collection: {e}")))?;
+
            std::fs::create_dir_all(dest)?;
+
            for (name, file_hash) in collection.iter() {
+
                let target = dest.join(name);
+
                if let Some(parent) = target.parent() {
+
                    std::fs::create_dir_all(parent)?;
+
                }
+
                let bytes = blobs
+
                    .get_bytes(*file_hash)
+
                    .await
+
                    .map_err(|e| Error::Iroh(format!("read entry {name}: {e}")))?;
+
                std::fs::write(&target, &bytes)?;
+
            }
+
        }
+
    }
+
    Ok(())
+
}
+

+
/// Walk every (did, url) pair on an artifact and recover an iroh provider
+
/// public key. Two URL conventions are supported:
+
///
+
/// - `iroh://` (no host) — derive provider key from the contributor DID
+
///   via `did_to_iroh_public_key`. radworks-app and any peer that reuses
+
///   the Radicle keystore for iroh write locations in this form.
+
/// - `iroh://{endpoint_id_z32}` — parse the URL host with
+
///   `iroh::PublicKey::from_str`. radicle-desktop writes locations in
+
///   this form because its iroh key is decoupled from the Radicle key.
+
///
+
/// HTTP/HTTPS URLs are skipped here; they route through a separate
+
/// fallback path (see `radicle_artifact::share::download_http`).
+
fn resolve_iroh_providers(locations: &BTreeMap<Did, BTreeSet<Url>>) -> Vec<iroh::PublicKey> {
+
    let mut out = Vec::new();
+
    for (did, urls) in locations {
+
        for url in urls {
+
            if url.scheme() != "iroh" {
+
                continue;
+
            }
+
            let key = match url.host_str() {
+
                None | Some("") => keys::did_to_iroh_public_key(did).ok(),
+
                Some(host) => iroh::PublicKey::from_str(host).ok(),
+
            };
+
            if let Some(k) = key {
+
                out.push(k);
+
            }
+
        }
+
    }
+
    out
+
}
modified crates/radicle-types/src/lib.rs
@@ -2,6 +2,8 @@ use traits::cobs::Cobs;
use traits::issue::{Issues, IssuesMut};
use traits::job::Jobs;
use traits::patch::{Patches, PatchesMut};
+
use traits::release::Releases;
+
use traits::release_mut::ReleasesMut;
use traits::repo::Repo;
use traits::thread::Thread;
use traits::Profile;
@@ -11,9 +13,12 @@ pub mod config;
pub mod diff;
pub mod domain;
pub mod error;
+
pub mod fetch;
pub mod oid;
pub mod outbound;
pub mod repo;
+
pub mod seeder;
+
pub mod settings;
pub mod source;
pub mod syntax;
pub mod test;
@@ -24,6 +29,16 @@ pub struct AppState {
    pub profile: radicle::Profile,
}

+
/// Iroh networking state. Held separately from `AppState` so command handlers
+
/// and tests that don't touch iroh don't need to construct it. Both are
+
/// `app.manage()`-d at startup, so there is no staged-unlock — the seeder is
+
/// always available once the frontend can call commands.
+
#[derive(Clone)]
+
pub struct IrohState {
+
    pub blobs: iroh_blobs::store::fs::FsStore,
+
    pub iroh_router: iroh::protocol::Router,
+
}
+

impl Repo for AppState {}
impl Thread for AppState {}
impl Cobs for AppState {}
@@ -32,6 +47,8 @@ impl IssuesMut for AppState {}
impl Jobs for AppState {}
impl Patches for AppState {}
impl PatchesMut for AppState {}
+
impl Releases for AppState {}
+
impl ReleasesMut for AppState {}
impl Profile for AppState {
    fn profile(&self) -> radicle::Profile {
        self.profile.clone()
modified crates/radicle-types/src/repo.rs
@@ -51,6 +51,27 @@ pub struct RepoInfo {
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "repo/")]
+
pub struct Tag {
+
    /// Short tag refname, e.g. `v1.0.0`.
+
    pub name: String,
+
    /// Tag OID for annotated tags, commit OID for lightweight tags.
+
    /// This is the OID stored on the artifact COB's `tag` field for
+
    /// annotated tags.
+
    pub oid: String,
+
    /// The commit this tag points at. For lightweight tags this equals
+
    /// `oid`; for annotated tags it's the commit reachable via the tag
+
    /// object's target.
+
    pub commit: String,
+
    pub annotated: bool,
+
    #[serde(skip_serializing_if = "Option::is_none")]
+
    #[ts(optional)]
+
    pub message: Option<String>,
+
}
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "repo/")]
pub struct Readme {
    #[ts(as = "String")]
    pub id: surf::Oid,
added crates/radicle-types/src/seeder.rs
@@ -0,0 +1,280 @@
+
use std::collections::HashSet;
+
use std::path::{Path, PathBuf};
+
use std::str::FromStr;
+

+
use cid::Cid;
+
use futures_lite::StreamExt;
+
use iroh::protocol::Router;
+
use iroh_blobs::api::blobs::{AddPathOptions, ImportMode};
+
use iroh_blobs::api::Store;
+
use iroh_blobs::format::collection::Collection;
+
use iroh_blobs::store::fs::FsStore;
+
use iroh_blobs::{BlobFormat, BlobsProtocol, Hash, HashAndFormat};
+
use radicle_artifact::share::cid_utils::{self, ArtifactKind};
+
use radicle_artifact::share::EndpointPreset;
+

+
use crate::error::Error;
+

+
const ARTIFACTS_DIR: &str = "artifacts";
+
const STORE_DIR: &str = "store";
+
const KEY_FILE: &str = "iroh.key";
+

+
pub struct Seeder {
+
    pub blobs: FsStore,
+
    pub router: Router,
+
}
+

+
/// Bootstrap the iroh seeder: generate or load the persisted iroh key, open
+
/// the FsStore, and spawn the BlobsProtocol router. Anything previously
+
/// tagged in the store is reachable the moment this returns.
+
pub async fn bootstrap(home: &Path) -> Result<Seeder, Error> {
+
    let dir = home.join(ARTIFACTS_DIR);
+
    std::fs::create_dir_all(&dir)?;
+

+
    let secret = load_or_generate_key(&dir.join(KEY_FILE))?;
+
    let blobs = FsStore::load(dir.join(STORE_DIR))
+
        .await
+
        .map_err(|e| Error::Iroh(e.to_string()))?;
+

+
    let preset = EndpointPreset::from_env().map_err(|e| Error::Iroh(e.to_string()))?;
+
    let endpoint = iroh::Endpoint::builder(preset)
+
        .secret_key(secret)
+
        .bind()
+
        .await
+
        .map_err(|e| Error::Iroh(e.to_string()))?;
+

+
    let blobs_protocol = BlobsProtocol::new(&blobs, None);
+
    let router = Router::builder(endpoint)
+
        .accept(iroh_blobs::ALPN, blobs_protocol)
+
        .spawn();
+

+
    Ok(Seeder { blobs, router })
+
}
+

+
fn load_or_generate_key(path: &PathBuf) -> Result<iroh::SecretKey, Error> {
+
    if path.exists() {
+
        let bytes = std::fs::read(path)?;
+
        let bytes: [u8; 32] = bytes
+
            .try_into()
+
            .map_err(|_| Error::MalformedIrohKey { path: path.clone() })?;
+
        Ok(iroh::SecretKey::from_bytes(&bytes))
+
    } else {
+
        let secret = iroh::SecretKey::generate();
+
        write_key(path, &secret)?;
+
        Ok(secret)
+
    }
+
}
+

+
#[cfg(unix)]
+
fn write_key(path: &Path, secret: &iroh::SecretKey) -> Result<(), Error> {
+
    use std::os::unix::fs::OpenOptionsExt;
+

+
    let mut opts = std::fs::OpenOptions::new();
+
    opts.write(true).create_new(true).mode(0o600);
+
    let mut f = opts.open(path)?;
+
    std::io::Write::write_all(&mut f, &secret.to_bytes())?;
+
    Ok(())
+
}
+

+
#[cfg(not(unix))]
+
fn write_key(path: &Path, secret: &iroh::SecretKey) -> Result<(), Error> {
+
    std::fs::write(path, secret.to_bytes())?;
+
    Ok(())
+
}
+

+
/// Tag prefix for blobs/collections we are seeding. The presence of the tag
+
/// + a running `BlobsProtocol` is what makes content reachable to peers.
+
const SEEDED_PREFIX: &str = "seeded/";
+

+
fn seeded_tag(cid: &Cid) -> String {
+
    format!("{SEEDED_PREFIX}{cid}")
+
}
+

+
/// Import a single file into the store, copying its bytes so the original
+
/// can be moved or deleted later. Verifies the import hash matches the
+
/// expected CID before returning.
+
pub async fn import_blob(store: &Store, path: &Path, expected: &Cid) -> Result<Hash, Error> {
+
    let tag = store
+
        .add_path_with_opts(AddPathOptions {
+
            path: path.to_path_buf(),
+
            format: BlobFormat::Raw,
+
            mode: ImportMode::Copy,
+
        })
+
        .with_tag()
+
        .await
+
        .map_err(|e| Error::Iroh(format!("import blob: {e}")))?;
+

+
    let actual = cid_utils::blake3_hash_to_cid(tag.hash, ArtifactKind::Blob);
+
    if actual != *expected {
+
        return Err(Error::CidMismatch {
+
            expected: expected.to_string(),
+
            actual: actual.to_string(),
+
        });
+
    }
+
    Ok(tag.hash)
+
}
+

+
/// Import a directory as a Collection, copying each file's bytes.
+
pub async fn import_collection(store: &Store, dir: &Path, expected: &Cid) -> Result<Hash, Error> {
+
    let entries = cid_utils::canonical_walk(dir)?;
+

+
    let mut pairs: Vec<(String, Hash)> = Vec::new();
+
    for (name, abs) in entries {
+
        let tag = store
+
            .add_path_with_opts(AddPathOptions {
+
                path: abs,
+
                format: BlobFormat::Raw,
+
                mode: ImportMode::Copy,
+
            })
+
            .with_tag()
+
            .await
+
            .map_err(|e| Error::Iroh(format!("import file {name}: {e}")))?;
+
        pairs.push((name, tag.hash));
+
    }
+

+
    let collection = Collection::from_iter(pairs);
+
    let root_tag = collection
+
        .store(store)
+
        .await
+
        .map_err(|e| Error::Iroh(format!("store collection: {e}")))?;
+

+
    let actual = cid_utils::blake3_hash_to_cid(root_tag.hash(), ArtifactKind::Collection);
+
    if actual != *expected {
+
        return Err(Error::CidMismatch {
+
            expected: expected.to_string(),
+
            actual: actual.to_string(),
+
        });
+
    }
+
    Ok(root_tag.hash())
+
}
+

+
/// Mark a CID as seeded by setting the `seeded/{cid}` tag pointing at the
+
/// blob hash with the format appropriate for its kind (raw vs hash-seq).
+
pub async fn register_seeded(store: &Store, cid: &Cid, hash: Hash) -> Result<(), Error> {
+
    let kind = cid_utils::artifact_kind(cid)?;
+
    let value = match kind {
+
        ArtifactKind::Blob => HashAndFormat::raw(hash),
+
        ArtifactKind::Collection => HashAndFormat::hash_seq(hash),
+
    };
+
    store
+
        .tags()
+
        .set(seeded_tag(cid).as_bytes(), value)
+
        .await
+
        .map_err(|e| Error::Iroh(format!("set seeded tag: {e}")))?;
+
    Ok(())
+
}
+

+
pub async fn unregister_seeded(store: &Store, cid: &Cid) -> Result<(), Error> {
+
    store
+
        .tags()
+
        .delete(seeded_tag(cid).as_bytes())
+
        .await
+
        .map_err(|e| Error::Iroh(format!("delete seeded tag: {e}")))?;
+
    Ok(())
+
}
+

+
pub async fn is_seeded(store: &Store, cid: &Cid) -> Result<bool, Error> {
+
    let info = store
+
        .tags()
+
        .get(seeded_tag(cid).as_bytes())
+
        .await
+
        .map_err(|e| Error::Iroh(format!("get seeded tag: {e}")))?;
+
    Ok(info.is_some())
+
}
+

+
/// String-taking wrapper for callers (Tauri commands) that don't pull in
+
/// the `cid` crate. Parses the CID, dispatches to the right import, sets
+
/// the seeded tag, and returns the import hash as a string for logging.
+
pub async fn seed(store: &Store, cid: &str, source: &Path) -> Result<(), Error> {
+
    let parsed_cid = Cid::from_str(cid)?;
+
    let kind = cid_utils::artifact_kind(&parsed_cid)?;
+
    let hash = match kind {
+
        ArtifactKind::Blob => import_blob(store, source, &parsed_cid).await?,
+
        ArtifactKind::Collection => import_collection(store, source, &parsed_cid).await?,
+
    };
+
    register_seeded(store, &parsed_cid, hash).await?;
+
    Ok(())
+
}
+

+
pub async fn unseed(store: &Store, cid: &str) -> Result<(), Error> {
+
    let parsed_cid = Cid::from_str(cid)?;
+
    unregister_seeded(store, &parsed_cid).await
+
}
+

+
pub async fn is_seeded_str(store: &Store, cid: &str) -> Result<bool, Error> {
+
    let parsed_cid = Cid::from_str(cid)?;
+
    is_seeded(store, &parsed_cid).await
+
}
+

+
/// Build the location URL we register on the COB when seeding. Form:
+
/// `iroh://{endpoint_id_z32}` — explicit because our iroh key is
+
/// independent from the Radicle DID, so the bare-`iroh://` derive-from-DID
+
/// convention used by radworks-app does not apply.
+
pub fn our_iroh_url(endpoint: &iroh::Endpoint) -> String {
+
    format!("iroh://{}", endpoint.id())
+
}
+

+
/// Sum of stored bytes for one seeded CID. Blobs report their own size;
+
/// collections walk their hash sequence and sum the children. Errors
+
/// resolve to `Ok(0)` so the global seeding view can still render the row.
+
pub async fn artifact_size(store: &Store, cid: &Cid) -> u64 {
+
    let Ok(kind) = cid_utils::artifact_kind(cid) else {
+
        return 0;
+
    };
+
    let Ok(Some(tag)) = store.tags().get(seeded_tag(cid).as_bytes()).await else {
+
        return 0;
+
    };
+
    match kind {
+
        ArtifactKind::Blob => blob_size(store, tag.hash).await,
+
        ArtifactKind::Collection => match Collection::load(tag.hash, store).await {
+
            Ok(collection) => {
+
                let mut total = 0u64;
+
                for (_, child) in collection.iter() {
+
                    total = total.saturating_add(blob_size(store, *child).await);
+
                }
+
                total
+
            }
+
            Err(_) => 0,
+
        },
+
    }
+
}
+

+
/// String-taking wrapper around [`artifact_size`] so Tauri commands that
+
/// don't depend on the `cid` crate can call into us with the raw CID.
+
pub async fn artifact_size_str(store: &Store, cid: &str) -> u64 {
+
    match Cid::from_str(cid) {
+
        Ok(parsed) => artifact_size(store, &parsed).await,
+
        Err(_) => 0,
+
    }
+
}
+

+
async fn blob_size(store: &Store, hash: Hash) -> u64 {
+
    use iroh_blobs::api::proto::BlobStatus;
+
    match store.blobs().status(hash).await {
+
        Ok(BlobStatus::Complete { size }) => size,
+
        Ok(BlobStatus::Partial { size }) => size.unwrap_or(0),
+
        _ => 0,
+
    }
+
}
+

+
/// Return the set of CIDs we currently have seeded locally. Decoding
+
/// failures (unlikely — we wrote the tags ourselves) are skipped.
+
pub async fn seeded_cids(store: &Store) -> Result<HashSet<Cid>, Error> {
+
    let mut stream = store
+
        .tags()
+
        .list_prefix(SEEDED_PREFIX.as_bytes())
+
        .await
+
        .map_err(|e| Error::Iroh(format!("list seeded tags: {e}")))?;
+

+
    let mut out = HashSet::new();
+
    while let Some(item) = stream.next().await {
+
        let info = item.map_err(|e| Error::Iroh(format!("seeded tag stream: {e}")))?;
+
        let name = String::from_utf8_lossy(info.name.as_ref());
+
        if let Some(suffix) = name.strip_prefix(SEEDED_PREFIX) {
+
            if let Ok(cid) = Cid::from_str(suffix) {
+
                out.insert(cid);
+
            }
+
        }
+
    }
+
    Ok(out)
+
}
added crates/radicle-types/src/settings.rs
@@ -0,0 +1,54 @@
+
use std::path::{Path, PathBuf};
+

+
use serde::{Deserialize, Serialize};
+

+
use crate::error::Error;
+

+
const SETTINGS_FILE: &str = "desktop.json";
+

+
#[derive(Debug, Clone, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
pub struct Settings {
+
    /// Whether newly-added artifacts are automatically imported into the
+
    /// FsStore and registered with an `iroh://` location. Defaults to true
+
    /// — turning it off keeps the create-artifact path COB-only.
+
    #[serde(default = "default_auto_seed")]
+
    pub auto_seed_artifacts: bool,
+
}
+

+
fn default_auto_seed() -> bool {
+
    true
+
}
+

+
impl Default for Settings {
+
    fn default() -> Self {
+
        Self {
+
            auto_seed_artifacts: default_auto_seed(),
+
        }
+
    }
+
}
+

+
fn path(home: &Path) -> PathBuf {
+
    home.join(SETTINGS_FILE)
+
}
+

+
/// Read settings from `<home>/desktop.json`. Missing file or parse failures
+
/// fall back to defaults — settings are non-critical.
+
pub fn load(home: &Path) -> Settings {
+
    let p = path(home);
+
    let Ok(bytes) = std::fs::read(&p) else {
+
        return Settings::default();
+
    };
+
    serde_json::from_slice(&bytes).unwrap_or_default()
+
}
+

+
/// Write settings atomically (write to temp, rename) so a crash mid-write
+
/// can't leave a half-written file.
+
pub fn save(home: &Path, settings: &Settings) -> Result<(), Error> {
+
    let p = path(home);
+
    let tmp = p.with_extension("json.tmp");
+
    let bytes = serde_json::to_vec_pretty(settings)?;
+
    std::fs::write(&tmp, bytes)?;
+
    std::fs::rename(&tmp, &p)?;
+
    Ok(())
+
}
modified crates/radicle-types/src/traits.rs
@@ -6,6 +6,8 @@ pub mod cobs;
pub mod issue;
pub mod job;
pub mod patch;
+
pub mod release;
+
pub mod release_mut;
pub mod repo;
pub mod thread;

added crates/radicle-types/src/traits/release.rs
@@ -0,0 +1,236 @@
+
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
+
use std::path::PathBuf;
+
use std::str::FromStr;
+

+
use radicle::identity;
+
use radicle::identity::doc;
+
use radicle::storage::{ReadRepository, ReadStorage};
+
use radicle_artifact::share::cid_utils;
+
use radicle_artifact::Releases as ReleasesStore;
+
use radicle_surf as surf;
+

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

+
pub trait Releases: Profile {
+
    /// List every release for a repo, newest-first by COB timestamp.
+
    fn list_releases(&self, rid: identity::RepoId) -> Result<Vec<release::Release>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let aliases = profile.aliases();
+

+
        let surf_repo = surf::Repository::open(repo.path())?;
+
        // Build the tag-OID → refname index once so the list view can show
+
        // names without an N+1 ref walk per release.
+
        let tag_index = build_tag_index(&surf_repo);
+
        let releases = ReleasesStore::open(&repo)?;
+

+
        let mut out = Vec::new();
+
        for item in releases.all()? {
+
            let (id, release) = item?;
+
            let tag_name = release
+
                .tag()
+
                .and_then(|oid| tag_index.get(&oid.to_string()).cloned());
+
            let commit_summary = commit_summary(&surf_repo, *release.oid());
+
            out.push(release::Release::new(
+
                radicle_artifact::ReleaseId::from(id),
+
                &release,
+
                &aliases,
+
                tag_name,
+
                commit_summary,
+
            ));
+
        }
+
        // Newest first. Underlying iterator order isn't guaranteed across
+
        // radicle-artifact versions, so sort explicitly instead of just
+
        // reversing.
+
        out.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
+
        Ok(out)
+
    }
+

+
    fn release_by_id(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
    ) -> Result<Option<release::Release>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let aliases = profile.aliases();
+

+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let releases = ReleasesStore::open(&repo)?;
+
        let Some(release) = releases.get(&id)? else {
+
            return Ok(None);
+
        };
+

+
        let surf_repo = surf::Repository::open(repo.path())?;
+
        let tag_name = release
+
            .tag()
+
            .and_then(|oid| build_tag_index(&surf_repo).get(&oid.to_string()).cloned());
+
        let commit_summary = commit_summary(&surf_repo, *release.oid());
+

+
        Ok(Some(release::Release::new(
+
            id,
+
            &release,
+
            &aliases,
+
            tag_name,
+
            commit_summary,
+
        )))
+
    }
+

+
    fn releases_by_commit(
+
        &self,
+
        rid: identity::RepoId,
+
        oid: radicle::git::Oid,
+
    ) -> Result<Vec<release::Release>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let aliases = profile.aliases();
+

+
        let surf_repo = surf::Repository::open(repo.path())?;
+
        let tag_index = build_tag_index(&surf_repo);
+
        let releases = ReleasesStore::open(&repo)?;
+

+
        let mut out = Vec::new();
+
        for item in releases.find_by_commit(oid)? {
+
            let (id, release) = item?;
+
            let tag_name = release
+
                .tag()
+
                .and_then(|tag_oid| tag_index.get(&tag_oid.to_string()).cloned());
+
            let commit_summary = commit_summary(&surf_repo, *release.oid());
+
            out.push(release::Release::new(
+
                id,
+
                &release,
+
                &aliases,
+
                tag_name,
+
                commit_summary,
+
            ));
+
        }
+
        Ok(out)
+
    }
+

+
    /// Compute the BLAKE3-derived CID for a local file or directory.
+
    /// Files become Blob CIDs, directories become Collection CIDs.
+
    fn compute_cid(&self, path: PathBuf) -> Result<String, Error> {
+
        let cid = if path.is_dir() {
+
            cid_utils::compute_content_id(&path)?
+
        } else {
+
            cid_utils::compute_blob_cid(&path)?
+
        };
+
        Ok(cid.to_string())
+
    }
+

+
    /// Snapshot the COB locations for a single artifact in a release. Used
+
    /// by the download command to build a provider list before running
+
    /// the iroh-blobs Downloader; cloned out so the COB lock is released
+
    /// before the (long) async fetch begins.
+
    fn artifact_locations(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
        cid: String,
+
    ) -> Result<BTreeMap<identity::Did, BTreeSet<url::Url>>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+

+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let parsed_cid = cid::Cid::from_str(&cid)?;
+

+
        let releases = ReleasesStore::open(&repo)?;
+
        let release = releases.get(&id)?.ok_or_else(|| Error::ReleaseNotFound {
+
            release_id: release_id.clone(),
+
        })?;
+
        let artifact =
+
            release
+
                .artifact(&parsed_cid)
+
                .ok_or_else(|| Error::ArtifactNotInRelease {
+
                    cid: cid.clone(),
+
                    release_id: release_id.clone(),
+
                })?;
+
        Ok(artifact.locations().clone())
+
    }
+

+
    /// Walk every locally-stored repo and return the artifact entries
+
    /// whose CID is in the supplied seeded set. The caller (Tauri command)
+
    /// supplies the set so this trait stays free of iroh dependencies.
+
    fn find_seeded_artifacts(
+
        &self,
+
        seeded: &HashSet<String>,
+
    ) -> Result<Vec<release::SeededArtifact>, Error> {
+
        let profile = self.profile();
+
        let storage = &profile.storage;
+

+
        let mut out = Vec::new();
+
        for radicle::storage::RepositoryInfo { rid, doc, .. } in storage.repositories()? {
+
            let Ok(repo) = profile.storage.repository(rid) else {
+
                continue;
+
            };
+
            let Ok(releases) = ReleasesStore::open(&repo) else {
+
                continue;
+
            };
+
            let repo_name = doc
+
                .payload()
+
                .get(&doc::PayloadId::project())
+
                .and_then(|payload| {
+
                    crate::repo::ProjectPayloadData::try_from((*payload).clone()).ok()
+
                })
+
                .map(|p| p.name)
+
                .unwrap_or_default();
+

+
            for item in releases.all()? {
+
                let Ok((id, release)) = item else { continue };
+
                for (cid, artifact) in release.artifacts() {
+
                    let cid_str = cid.to_string();
+
                    if !seeded.contains(&cid_str) {
+
                        continue;
+
                    }
+
                    let kind = match cid_utils::artifact_kind(cid) {
+
                        Ok(cid_utils::ArtifactKind::Blob) => release::ArtifactKind::Blob,
+
                        Ok(cid_utils::ArtifactKind::Collection) => {
+
                            release::ArtifactKind::Collection
+
                        }
+
                        Err(_) => release::ArtifactKind::Unknown,
+
                    };
+
                    out.push(release::SeededArtifact {
+
                        rid,
+
                        repo_name: repo_name.clone(),
+
                        release_id: id.to_string(),
+
                        cid: cid_str,
+
                        name: artifact.name().to_string(),
+
                        kind,
+
                        // Size is filled in by the Tauri command after this
+
                        // returns; the trait can't reach the iroh store.
+
                        size_bytes: 0,
+
                    });
+
                }
+
            }
+
        }
+
        Ok(out)
+
    }
+
}
+

+
/// Build a lookup from tag-object OID to short refname so the list view
+
/// can show e.g. `v1.0.0` instead of a 40-char hash. Annotated tag OIDs
+
/// land under their tag-object OID; lightweight tags are keyed by the
+
/// commit they point at, matching `Release::tag`'s storage convention.
+
fn build_tag_index(repo: &surf::Repository) -> HashMap<String, String> {
+
    use surf::{Glob, Tag};
+

+
    let mut out = HashMap::new();
+
    let Ok(tags) = repo.tags(&Glob::all_tags()) else {
+
        return out;
+
    };
+
    for tag in tags.flatten() {
+
        let (id, name) = match tag {
+
            Tag::Light { id, name } => (id, name),
+
            Tag::Annotated { id, name, .. } => (id, name),
+
        };
+
        out.insert(id.to_string(), name.to_string());
+
    }
+
    out
+
}
+

+
fn commit_summary(repo: &surf::Repository, oid: radicle::git::Oid) -> Option<String> {
+
    let surf_oid = crate::oid::into_surf(oid);
+
    repo.commit(surf_oid).ok().map(|c| c.summary)
+
}
added crates/radicle-types/src/traits/release_mut.rs
@@ -0,0 +1,192 @@
+
use std::str::FromStr;
+

+
use radicle::identity;
+
use radicle::storage::ReadStorage;
+
use radicle_artifact::Releases as ReleasesStore;
+
use url::Url;
+

+
use crate::error::Error;
+
use crate::traits::release::Releases;
+

+
pub trait ReleasesMut: Releases {
+
    /// Find a release for the commit OID, or create it. Returns the
+
    /// release id as a string for the frontend. Idempotent.
+
    ///
+
    /// `tag` is recorded on the release COB when the user selects an
+
    /// annotated tag for the release; for lightweight tags or bare
+
    /// commit OIDs pass `None`. Only honoured when creating a release —
+
    /// when an existing one is reused, its tag stays as-is.
+
    ///
+
    /// radicle-artifact 0.12 dropped its built-in `find_or_create_by_oid`
+
    /// in favour of an explicit two-step pattern; we recreate that here.
+
    fn create_or_open_release(
+
        &self,
+
        rid: identity::RepoId,
+
        oid: radicle::git::Oid,
+
        tag: Option<radicle::git::Oid>,
+
    ) -> Result<String, Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let mut releases = ReleasesStore::open(&repo)?;
+

+
        let existing = {
+
            let mut iter = releases.find_by_commit(oid)?;
+
            match iter.next() {
+
                Some(item) => Some(item?.0),
+
                None => None,
+
            }
+
        };
+
        let id = match existing {
+
            Some(id) => id,
+
            None => {
+
                let release = releases.create(oid, tag, &signer)?;
+
                *release.id()
+
            }
+
        };
+
        Ok(id.to_string())
+
    }
+

+
    fn add_artifact(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
        cid: String,
+
        name: String,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let cid = cid::Cid::from_str(&cid)?;
+

+
        let mut releases = ReleasesStore::open(&repo)?;
+
        let mut release = releases.get_mut(&id)?;
+
        release.add_artifact(cid, name, &signer)?;
+
        Ok(())
+
    }
+

+
    fn add_location(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
        cid: String,
+
        url: String,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let cid = cid::Cid::from_str(&cid)?;
+
        let url = Url::parse(&url)?;
+

+
        let mut releases = ReleasesStore::open(&repo)?;
+
        let mut release = releases.get_mut(&id)?;
+
        release.add_location(cid, url, &signer)?;
+
        Ok(())
+
    }
+

+
    fn remove_location(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
        cid: String,
+
        url: String,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let cid = cid::Cid::from_str(&cid)?;
+
        let url = Url::parse(&url)?;
+

+
        let mut releases = ReleasesStore::open(&repo)?;
+
        let mut release = releases.get_mut(&id)?;
+
        release.remove_location(cid, url, &signer)?;
+
        Ok(())
+
    }
+

+
    fn attest_artifact(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
        cid: String,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let cid = cid::Cid::from_str(&cid)?;
+

+
        let mut releases = ReleasesStore::open(&repo)?;
+
        let mut release = releases.get_mut(&id)?;
+
        release.attest(cid, &signer)?;
+
        Ok(())
+
    }
+

+
    fn set_metadata(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
        cid: String,
+
        key: String,
+
        value: serde_json::Value,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let cid = cid::Cid::from_str(&cid)?;
+

+
        let mut releases = ReleasesStore::open(&repo)?;
+
        let mut release = releases.get_mut(&id)?;
+
        release.set_metadata(cid, key, value, &signer)?;
+
        Ok(())
+
    }
+

+
    fn remove_metadata(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
        cid: String,
+
        key: String,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let cid = cid::Cid::from_str(&cid)?;
+

+
        let mut releases = ReleasesStore::open(&repo)?;
+
        let mut release = releases.get_mut(&id)?;
+
        release.remove_metadata(cid, key, &signer)?;
+
        Ok(())
+
    }
+

+
    fn redact_artifact(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
        cid: String,
+
        reason: String,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let cid = cid::Cid::from_str(&cid)?;
+

+
        let mut releases = ReleasesStore::open(&repo)?;
+
        let mut release = releases.get_mut(&id)?;
+
        release.redact(cid, reason, &signer)?;
+
        Ok(())
+
    }
+
}
modified crates/radicle-types/src/traits/repo.rs
@@ -480,6 +480,48 @@ pub trait Repo: Profile {
        Ok(commit.into())
    }

+
    /// List the repo's tags. Used by the New Release form so users can
+
    /// pick an annotated tag (which records its OID alongside the commit
+
    /// on the artifact COB) instead of typing a raw commit OID.
+
    fn list_tags(&self, rid: identity::RepoId) -> Result<Vec<repo::Tag>, Error> {
+
        use radicle_surf::{Glob, Tag};
+

+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let surf_repo = surf::Repository::open(repo.path())?;
+

+
        let mut tags = Vec::new();
+
        for tag in surf_repo.tags(&Glob::all_tags())? {
+
            let Ok(tag) = tag else { continue };
+
            let entry = match tag {
+
                Tag::Light { id, name } => repo::Tag {
+
                    name: name.to_string(),
+
                    oid: id.to_string(),
+
                    commit: id.to_string(),
+
                    annotated: false,
+
                    message: None,
+
                },
+
                Tag::Annotated {
+
                    id,
+
                    target,
+
                    name,
+
                    message,
+
                    ..
+
                } => repo::Tag {
+
                    name: name.to_string(),
+
                    oid: id.to_string(),
+
                    commit: target.to_string(),
+
                    annotated: true,
+
                    message,
+
                },
+
            };
+
            tags.push(entry);
+
        }
+
        // Newest tag first by name (semver-ish sort) — surf already returns
+
        // them sorted; keep the natural order as-is.
+
        Ok(tags)
+
    }
+

    fn unseed(&self, rid: identity::RepoId) -> Result<(), Error> {
        let profile = self.profile();
        let mut node = radicle::Node::new(profile.home().socket_from_env());
modified crates/test-http-api/src/api.rs
@@ -27,6 +27,8 @@ use radicle_types::traits::cobs::Cobs;
use radicle_types::traits::issue::{Issues, IssuesMut};
use radicle_types::traits::job::Jobs;
use radicle_types::traits::patch::{Patches, PatchesMut};
+
use radicle_types::traits::release::Releases;
+
use radicle_types::traits::release_mut::ReleasesMut;
use radicle_types::traits::repo::{Repo, Show};
use radicle_types::traits::thread::Thread;
use radicle_types::traits::Profile;
@@ -45,6 +47,8 @@ impl IssuesMut for Context {}
impl Jobs for Context {}
impl Patches for Context {}
impl PatchesMut for Context {}
+
impl Releases for Context {}
+
impl ReleasesMut for Context {}
impl Profile for Context {
    fn profile(&self) -> radicle::Profile {
        self.profile.deref().clone()
@@ -102,6 +106,30 @@ pub fn router(ctx: Context) -> Router {
        .route("/save_embed_by_bytes", post(save_embed_handler))
        .route("/save_embed_to_disk", post(save_embed_handler))
        .route("/list_jobs", post(jobs_handler))
+
        .route("/list_tags", post(list_tags_handler))
+
        .route("/list_releases", post(list_releases_handler))
+
        .route("/release_by_id", post(release_by_id_handler))
+
        .route("/releases_by_commit", post(releases_by_commit_handler))
+
        .route("/compute_artifact_cid", post(compute_artifact_cid_handler))
+
        .route(
+
            "/create_or_open_release",
+
            post(create_or_open_release_handler),
+
        )
+
        .route("/add_artifact", post(add_artifact_handler))
+
        .route("/add_location", post(add_location_handler))
+
        .route("/remove_location", post(remove_location_handler))
+
        .route("/attest_artifact", post(attest_artifact_handler))
+
        .route("/set_metadata", post(set_metadata_handler))
+
        .route("/remove_metadata", post(remove_metadata_handler))
+
        .route("/redact_artifact", post(redact_artifact_handler))
+
        .route(
+
            "/get_auto_seed_artifacts",
+
            post(get_auto_seed_artifacts_handler),
+
        )
+
        .route(
+
            "/set_auto_seed_artifacts",
+
            post(set_auto_seed_artifacts_handler),
+
        )
        .route("/list_notifications", post(list_notifications_handler))
        .route("/notification_count", post(notification_count_handler))
        .route("/clear_notifications", post(clear_notifications_handler))
@@ -598,3 +626,241 @@ async fn jobs_handler(

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

+
async fn list_tags_handler(
+
    State(ctx): State<Context>,
+
    Json(RepoBody { rid }): Json<RepoBody>,
+
) -> impl IntoResponse {
+
    Ok::<_, Error>(Json(ctx.list_tags(rid)?))
+
}
+

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

+
#[derive(Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
struct ReleaseByIdBody {
+
    pub rid: identity::RepoId,
+
    pub release_id: String,
+
}
+

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

+
#[derive(Serialize, Deserialize)]
+
struct ComputeCidBody {
+
    pub path: PathBuf,
+
}
+

+
#[derive(Serialize, Deserialize)]
+
struct CreateOrOpenReleaseBody {
+
    pub rid: identity::RepoId,
+
    pub oid: git::Oid,
+
    #[serde(default)]
+
    pub tag: Option<git::Oid>,
+
}
+

+
#[derive(Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
struct AddArtifactBody {
+
    pub rid: identity::RepoId,
+
    pub release_id: String,
+
    pub cid: String,
+
    pub name: String,
+
}
+

+
#[derive(Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
struct LocationBody {
+
    pub rid: identity::RepoId,
+
    pub release_id: String,
+
    pub cid: String,
+
    pub url: String,
+
}
+

+
#[derive(Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
struct AttestBody {
+
    pub rid: identity::RepoId,
+
    pub release_id: String,
+
    pub cid: String,
+
}
+

+
#[derive(Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
struct RedactBody {
+
    pub rid: identity::RepoId,
+
    pub release_id: String,
+
    pub cid: String,
+
    pub reason: String,
+
}
+

+
#[derive(Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
struct SetMetadataBody {
+
    pub rid: identity::RepoId,
+
    pub release_id: String,
+
    pub cid: String,
+
    pub key: String,
+
    pub value: serde_json::Value,
+
}
+

+
#[derive(Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
struct RemoveMetadataBody {
+
    pub rid: identity::RepoId,
+
    pub release_id: String,
+
    pub cid: String,
+
    pub key: String,
+
}
+

+
#[derive(Serialize, Deserialize)]
+
struct AutoSeedBody {
+
    pub enabled: bool,
+
}
+

+
async fn list_releases_handler(
+
    State(ctx): State<Context>,
+
    Json(RidBody { rid }): Json<RidBody>,
+
) -> impl IntoResponse {
+
    Ok::<_, Error>(Json(ctx.list_releases(rid)?))
+
}
+

+
async fn release_by_id_handler(
+
    State(ctx): State<Context>,
+
    Json(ReleaseByIdBody { rid, release_id }): Json<ReleaseByIdBody>,
+
) -> impl IntoResponse {
+
    Ok::<_, Error>(Json(ctx.release_by_id(rid, release_id)?))
+
}
+

+
async fn releases_by_commit_handler(
+
    State(ctx): State<Context>,
+
    Json(ReleasesByCommitBody { rid, sha }): Json<ReleasesByCommitBody>,
+
) -> impl IntoResponse {
+
    Ok::<_, Error>(Json(ctx.releases_by_commit(rid, sha)?))
+
}
+

+
async fn compute_artifact_cid_handler(
+
    State(ctx): State<Context>,
+
    Json(ComputeCidBody { path }): Json<ComputeCidBody>,
+
) -> impl IntoResponse {
+
    Ok::<_, Error>(Json(ctx.compute_cid(path)?))
+
}
+

+
async fn create_or_open_release_handler(
+
    State(ctx): State<Context>,
+
    Json(CreateOrOpenReleaseBody { rid, oid, tag }): Json<CreateOrOpenReleaseBody>,
+
) -> impl IntoResponse {
+
    Ok::<_, Error>(Json(ctx.create_or_open_release(rid, oid, tag)?))
+
}
+

+
async fn add_artifact_handler(
+
    State(ctx): State<Context>,
+
    Json(AddArtifactBody {
+
        rid,
+
        release_id,
+
        cid,
+
        name,
+
    }): Json<AddArtifactBody>,
+
) -> impl IntoResponse {
+
    ctx.add_artifact(rid, release_id, cid, name)?;
+
    Ok::<_, Error>(Json(()))
+
}
+

+
async fn add_location_handler(
+
    State(ctx): State<Context>,
+
    Json(LocationBody {
+
        rid,
+
        release_id,
+
        cid,
+
        url,
+
    }): Json<LocationBody>,
+
) -> impl IntoResponse {
+
    ctx.add_location(rid, release_id, cid, url)?;
+
    Ok::<_, Error>(Json(()))
+
}
+

+
async fn remove_location_handler(
+
    State(ctx): State<Context>,
+
    Json(LocationBody {
+
        rid,
+
        release_id,
+
        cid,
+
        url,
+
    }): Json<LocationBody>,
+
) -> impl IntoResponse {
+
    ctx.remove_location(rid, release_id, cid, url)?;
+
    Ok::<_, Error>(Json(()))
+
}
+

+
async fn attest_artifact_handler(
+
    State(ctx): State<Context>,
+
    Json(AttestBody {
+
        rid,
+
        release_id,
+
        cid,
+
    }): Json<AttestBody>,
+
) -> impl IntoResponse {
+
    ctx.attest_artifact(rid, release_id, cid)?;
+
    Ok::<_, Error>(Json(()))
+
}
+

+
async fn set_metadata_handler(
+
    State(ctx): State<Context>,
+
    Json(SetMetadataBody {
+
        rid,
+
        release_id,
+
        cid,
+
        key,
+
        value,
+
    }): Json<SetMetadataBody>,
+
) -> impl IntoResponse {
+
    ctx.set_metadata(rid, release_id, cid, key, value)?;
+
    Ok::<_, Error>(Json(()))
+
}
+

+
async fn remove_metadata_handler(
+
    State(ctx): State<Context>,
+
    Json(RemoveMetadataBody {
+
        rid,
+
        release_id,
+
        cid,
+
        key,
+
    }): Json<RemoveMetadataBody>,
+
) -> impl IntoResponse {
+
    ctx.remove_metadata(rid, release_id, cid, key)?;
+
    Ok::<_, Error>(Json(()))
+
}
+

+
async fn redact_artifact_handler(
+
    State(ctx): State<Context>,
+
    Json(RedactBody {
+
        rid,
+
        release_id,
+
        cid,
+
        reason,
+
    }): Json<RedactBody>,
+
) -> impl IntoResponse {
+
    ctx.redact_artifact(rid, release_id, cid, reason)?;
+
    Ok::<_, Error>(Json(()))
+
}
+

+
async fn get_auto_seed_artifacts_handler(State(ctx): State<Context>) -> impl IntoResponse {
+
    let settings = radicle_types::settings::load(ctx.profile().home().path());
+
    Ok::<_, Error>(Json(settings.auto_seed_artifacts))
+
}
+

+
async fn set_auto_seed_artifacts_handler(
+
    State(ctx): State<Context>,
+
    Json(AutoSeedBody { enabled }): Json<AutoSeedBody>,
+
) -> impl IntoResponse {
+
    let mut settings = radicle_types::settings::load(ctx.profile().home().path());
+
    settings.auto_seed_artifacts = enabled;
+
    radicle_types::settings::save(ctx.profile().home().path(), &settings)?;
+
    Ok::<_, Error>(Json(()))
+
}
modified rust-toolchain.toml
@@ -1,4 +1,4 @@
[toolchain]
-
channel = "1.90"
+
channel = "1.91"
profile = "default"
components = [ "rust-src" ]
modified src/App.svelte
@@ -42,6 +42,8 @@
  import Issues from "@app/views/repo/Issues.svelte";
  import Patch from "@app/views/repo/Patch.svelte";
  import Patches from "@app/views/repo/Patches.svelte";
+
  import Release from "@app/views/repo/Release.svelte";
+
  import Releases from "@app/views/repo/Releases.svelte";
  import RepoCommit from "@app/views/repo/RepoCommit.svelte";
  import RepoCommits from "@app/views/repo/RepoCommits.svelte";
  import RepoHome from "@app/views/repo/RepoHome.svelte";
@@ -227,6 +229,10 @@
      <Patch {...$activeRouteStore.params} />
    {:else if $activeRouteStore.resource === "repo.patches"}
      <Patches {...$activeRouteStore.params} />
+
    {:else if $activeRouteStore.resource === "repo.releases"}
+
      <Releases {...$activeRouteStore.params} />
+
    {:else if $activeRouteStore.resource === "repo.release"}
+
      <Release {...$activeRouteStore.params} />
    {:else}
      {unreachable($activeRouteStore)}
    {/if}
added src/components/AutoSeedSwitch.svelte
@@ -0,0 +1,51 @@
+
<script lang="ts">
+
  import { onMount } from "svelte";
+

+
  import { invoke } from "@app/lib/invoke";
+

+
  import Button from "@app/components/Button.svelte";
+

+
  let enabled = $state<boolean | undefined>(undefined);
+

+
  onMount(async () => {
+
    try {
+
      enabled = await invoke<boolean>("get_auto_seed_artifacts");
+
    } catch (err) {
+
      console.error("get_auto_seed_artifacts failed", err);
+
      enabled = true;
+
    }
+
  });
+

+
  async function set(value: boolean) {
+
    enabled = value;
+
    try {
+
      await invoke("set_auto_seed_artifacts", { enabled: value });
+
    } catch (err) {
+
      console.error("set_auto_seed_artifacts failed", err);
+
    }
+
  }
+
</script>
+

+
<style>
+
  .container {
+
    display: flex;
+
    align-items: center;
+
  }
+
</style>
+

+
<div class="container">
+
  <Button
+
    variant="ghost"
+
    flatRight
+
    active={enabled === true}
+
    onclick={() => set(true)}>
+
    On
+
  </Button>
+
  <Button
+
    variant="ghost"
+
    flatLeft
+
    active={enabled === false}
+
    onclick={() => set(false)}>
+
    Off
+
  </Button>
+
</div>
added src/components/CommitPicker.svelte
@@ -0,0 +1,129 @@
+
<script lang="ts">
+
  import type { Commit } from "@bindings/repo/Commit";
+

+
  import fuzzysort from "fuzzysort";
+

+
  interface Props {
+
    commits: Commit[];
+
    value: string;
+
    disabled?: boolean;
+
    onSelect: (oid: string) => void;
+
  }
+

+
  const { commits, value, disabled = false, onSelect }: Props = $props();
+

+
  let query = $state(value);
+
  let open = $state(false);
+

+
  // Keep the typed value in sync when the parent overwrites it (e.g. tag
+
  // selection auto-fills the OID). Without this the picker would keep
+
  // showing the user's stale input.
+
  $effect(() => {
+
    query = value;
+
  });
+

+
  const results = $derived(
+
    fuzzysort.go(query, commits, {
+
      keys: ["id", "summary"],
+
      threshold: 0.5,
+
      all: true,
+
      limit: 20,
+
    }),
+
  );
+

+
  function pick(oid: string) {
+
    query = oid;
+
    open = false;
+
    onSelect(oid);
+
  }
+
</script>
+

+
<style>
+
  .wrapper {
+
    position: relative;
+
  }
+
  input {
+
    width: 100%;
+
    padding: 0.4rem 0.5rem;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    background-color: var(--color-surface-canvas);
+
    font: var(--txt-body-m-regular);
+
  }
+
  .menu {
+
    position: absolute;
+
    z-index: 10;
+
    top: calc(100% + 0.25rem);
+
    left: 0;
+
    right: 0;
+
    max-height: 18rem;
+
    overflow-y: auto;
+
    background-color: var(--color-surface-canvas);
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    box-shadow: var(--elevation-low);
+
  }
+
  .item {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.125rem;
+
    padding: 0.5rem 0.75rem;
+
    cursor: pointer;
+
    text-align: left;
+
    background: none;
+
    border: none;
+
    width: 100%;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
  }
+
  .item:last-child {
+
    border-bottom: none;
+
  }
+
  .item:hover {
+
    background-color: var(--color-surface-subtle);
+
  }
+
  .summary {
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-primary);
+
    white-space: nowrap;
+
    overflow: hidden;
+
    text-overflow: ellipsis;
+
  }
+
  .oid {
+
    font: var(--txt-body-s-mono);
+
    color: var(--color-text-secondary);
+
  }
+
  .empty {
+
    padding: 0.75rem;
+
    color: var(--color-text-secondary);
+
    font: var(--txt-body-s-regular);
+
  }
+
</style>
+

+
<div class="wrapper">
+
  <input
+
    type="text"
+
    placeholder="Search commits or paste an OID"
+
    bind:value={query}
+
    oninput={() => onSelect(query)}
+
    onfocus={() => (open = true)}
+
    onblur={() => setTimeout(() => (open = false), 150)}
+
    {disabled} />
+
  {#if open && !disabled}
+
    <div class="menu">
+
      {#each results as result}
+
        <button
+
          type="button"
+
          class="item"
+
          onmousedown={e => {
+
            e.preventDefault();
+
            pick(result.obj.id);
+
          }}>
+
          <span class="summary">{result.obj.summary}</span>
+
          <span class="oid">{result.obj.id.slice(0, 12)}…</span>
+
        </button>
+
      {:else}
+
        <div class="empty">No matching commits</div>
+
      {/each}
+
    </div>
+
  {/if}
+
</div>
added src/components/ReleaseTeaser.svelte
@@ -0,0 +1,141 @@
+
<script lang="ts">
+
  import type { ReleaseFilter } from "@app/views/repo/router";
+
  import type { Release } from "@bindings/cob/release/Release";
+

+
  import { push } from "@app/lib/router";
+
  import { authorForNodeId, formatTimestamp } from "@app/lib/utils";
+

+
  import Icon from "@app/components/Icon.svelte";
+
  import Id from "@app/components/Id.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";
+

+
  interface Props {
+
    release: Release;
+
    rid: string;
+
    filter: ReleaseFilter;
+
  }
+

+
  const { release, rid, filter }: Props = $props();
+
</script>
+

+
<style>
+
  .teaser {
+
    display: flex;
+
    align-items: flex-start;
+
    justify-content: space-between;
+
    gap: 0.5rem;
+
    min-height: 5rem;
+
    background-color: var(--color-surface-canvas);
+
    padding: 1rem 1.25rem;
+
    cursor: pointer;
+
    font: var(--txt-body-l-regular);
+
    word-break: break-word;
+
    width: 100%;
+
  }
+
  .teaser:hover {
+
    background-color: var(--color-surface-subtle);
+
  }
+
  .teaser:first-of-type {
+
    border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0;
+
  }
+
  .teaser:last-of-type {
+
    border-radius: 0 0 var(--border-radius-sm) var(--border-radius-sm);
+
  }
+
  .teaser:only-of-type {
+
    border-radius: var(--border-radius-sm);
+
  }
+
  .left {
+
    display: flex;
+
    align-items: flex-start;
+
    gap: 0.75rem;
+
  }
+
  .body {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.25rem;
+
    align-items: flex-start;
+
  }
+
  .title {
+
    font: var(--txt-body-l-semibold);
+
    color: var(--color-text-primary);
+
  }
+
  .meta {
+
    display: flex;
+
    flex-wrap: wrap;
+
    align-items: center;
+
    gap: 0.375rem;
+
    color: var(--color-text-secondary);
+
    font: var(--txt-body-m-regular);
+
  }
+
  .tag-pill {
+
    font: var(--txt-body-s-mono);
+
    color: var(--color-text-secondary);
+
    background-color: var(--color-surface-subtle);
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    padding: 0.0625rem 0.375rem;
+
  }
+
  .right {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
+
    white-space: nowrap;
+
  }
+
  .count-chip {
+
    display: inline-flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    height: 1.5rem;
+
    padding: 0 0.5rem;
+
    color: var(--color-text-tertiary);
+
  }
+
</style>
+

+
<!-- svelte-ignore a11y_click_events_have_key_events -->
+
<div
+
  tabindex="0"
+
  role="button"
+
  class="teaser"
+
  onclick={() => {
+
    void push({
+
      resource: "repo.release",
+
      rid,
+
      release: release.id,
+
      filter,
+
    });
+
  }}>
+
  <div class="left">
+
    <div class="global-chip" style:padding="0">
+
      <Icon name="commit" />
+
    </div>
+
    <div class="body">
+
      <span class="title">
+
        {release.tagName ??
+
          release.commitSummary ??
+
          `Release ${release.oid.slice(0, 7)}`}
+
      </span>
+
      <div class="meta">
+
        <NodeId {...authorForNodeId(release.creator)} />
+
        released
+
        <Id id={release.id} clipboard={release.id} ariaLabel="Release ID" />
+
        from commit
+
        <Id id={release.oid} clipboard={release.oid} ariaLabel="Commit OID" />
+
        {#if release.tagName}
+
          <span class="tag-pill">{release.tagName}</span>
+
        {/if}
+
        {formatTimestamp(release.timestamp * 1000)}
+
      </div>
+
    </div>
+
  </div>
+

+
  <div class="right">
+
    <span class="count-chip">
+
      <Icon name="archive" />
+
      {release.artifacts.length}
+
    </span>
+
  </div>
+
</div>
modified src/components/SidebarRepoList.svelte
@@ -209,6 +209,14 @@
      activeRid() === rid
    );
  }
+

+
  function isReleases(rid: string): boolean {
+
    return (
+
      ($activeRoute.resource === "repo.releases" ||
+
        $activeRoute.resource === "repo.release") &&
+
      activeRid() === rid
+
    );
+
  }
</script>

<style>
@@ -630,12 +638,22 @@
      isPatches(repo.rid),
      activeProject?.meta.patches.open || undefined,
    )}
+
    {@render subItem(
+
      router.routeToPath({
+
        resource: "repo.releases",
+
        rid: repo.rid,
+
      }),
+
      "binary",
+
      "Releases",
+
      isReleases(repo.rid),
+
      undefined,
+
    )}
  {/if}
{/snippet}

{#snippet subItem(
  href: string,
-
  icon: "branch" | "issue" | "patch",
+
  icon: "branch" | "issue" | "patch" | "binary",
  label: string,
  active: boolean,
  count: number | undefined,
modified src/lib/router.ts
@@ -169,7 +169,9 @@ export function routeToPath(route: Route): string {
    route.resource === "repo.issue" ||
    route.resource === "repo.issues" ||
    route.resource === "repo.patch" ||
-
    route.resource === "repo.patches"
+
    route.resource === "repo.patches" ||
+
    route.resource === "repo.releases" ||
+
    route.resource === "repo.release"
  ) {
    return repoRouteToPath(route);
  } else if (route.resource === "booting") {
modified src/lib/router/definitions.ts
@@ -8,6 +8,8 @@ import {
  loadIssues,
  loadPatch,
  loadPatches,
+
  loadRelease,
+
  loadReleases,
  loadRepoCommit,
  loadRepoCommits,
  loadRepoHome,
@@ -64,7 +66,9 @@ export function isLoadedRepoRoute(
    route.resource === "repo.issue" ||
    route.resource === "repo.issues" ||
    route.resource === "repo.patch" ||
-
    route.resource === "repo.patches"
+
    route.resource === "repo.patches" ||
+
    route.resource === "repo.releases" ||
+
    route.resource === "repo.release"
  );
}

@@ -119,6 +123,19 @@ export async function loadRoute(
    return loadPatch(route);
  } else if (route.resource === "repo.patches") {
    return loadPatches(route);
+
  } else if (route.resource === "repo.releases") {
+
    return loadReleases(route);
+
  } else if (route.resource === "repo.release") {
+
    const loaded = await loadRelease(route);
+
    if (loaded) {
+
      return loaded;
+
    }
+
    // Fall back to the list when a release id no longer exists.
+
    return loadReleases({
+
      resource: "repo.releases",
+
      rid: route.rid,
+
      filter: route.filter,
+
    });
  }
  return route;
}
added src/modals/SeedingSettings.svelte
@@ -0,0 +1,201 @@
+
<script lang="ts">
+
  import type { SeededArtifact } from "@bindings/cob/release/SeededArtifact";
+

+
  import { onMount } from "svelte";
+

+
  import { invoke } from "@app/lib/invoke";
+
  import { hide } from "@app/lib/modal";
+

+
  import Button from "@app/components/Button.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import Id from "@app/components/Id.svelte";
+
  import ScrollArea from "@app/components/ScrollArea.svelte";
+

+
  let artifacts = $state<SeededArtifact[]>([]);
+
  let loading = $state(true);
+
  const busy = $state<Record<string, boolean>>({});
+

+
  onMount(() => {
+
    void refresh();
+
  });
+

+
  async function refresh() {
+
    loading = true;
+
    try {
+
      artifacts = await invoke<SeededArtifact[]>("list_seeded_artifacts");
+
    } catch (err) {
+
      console.error("list_seeded_artifacts failed", err);
+
      artifacts = [];
+
    } finally {
+
      loading = false;
+
    }
+
  }
+

+
  async function unseed(artifact: SeededArtifact) {
+
    const key = `${artifact.rid}/${artifact.cid}`;
+
    busy[key] = true;
+
    try {
+
      await invoke("unseed_artifact", {
+
        rid: artifact.rid,
+
        releaseId: artifact.releaseId,
+
        cid: artifact.cid,
+
      });
+
      artifacts = artifacts.filter(a => a.cid !== artifact.cid);
+
    } catch (err) {
+
      console.error("unseed failed", err);
+
    } finally {
+
      busy[key] = false;
+
    }
+
  }
+

+
  function formatBytes(bytes: number): string {
+
    if (bytes === 0) return "—";
+
    const units = ["B", "KiB", "MiB", "GiB", "TiB"];
+
    let value = bytes;
+
    let unit = 0;
+
    while (value >= 1024 && unit < units.length - 1) {
+
      value /= 1024;
+
      unit += 1;
+
    }
+
    const formatted = value < 10 ? value.toFixed(1) : value.toFixed(0);
+
    return `${formatted} ${units[unit]}`;
+
  }
+

+
  const totalBytes = $derived(
+
    artifacts.reduce((sum, a) => sum + a.sizeBytes, 0),
+
  );
+
</script>
+

+
<style>
+
  .modal {
+
    width: 48rem;
+
    max-height: 80vh;
+
    display: flex;
+
    flex-direction: column;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-lg);
+
    background-color: var(--color-surface-canvas);
+
    overflow: hidden;
+
  }
+
  .header {
+
    display: flex;
+
    align-items: center;
+
    justify-content: space-between;
+
    padding: 0 1.5rem;
+
    height: 3.25rem;
+
    flex-shrink: 0;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
  }
+
  .title {
+
    font: var(--txt-heading-s);
+
    color: var(--color-text-primary);
+
  }
+
  .summary {
+
    padding: 1rem 1.5rem;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
    display: flex;
+
    justify-content: space-between;
+
    align-items: center;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
+
  }
+
  .list {
+
    overflow-y: auto;
+
    flex: 1;
+
  }
+
  .row {
+
    display: grid;
+
    grid-template-columns: 1fr auto auto;
+
    gap: 1rem;
+
    padding: 0.75rem 1.5rem;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
    align-items: center;
+
  }
+
  .row:last-child {
+
    border-bottom: none;
+
  }
+
  .row .name {
+
    font: var(--txt-body-m-semibold);
+
    color: var(--color-text-primary);
+
    word-break: break-word;
+
  }
+
  .row .meta {
+
    display: flex;
+
    flex-wrap: wrap;
+
    gap: 0.5rem;
+
    align-items: center;
+
    font: var(--txt-body-s-regular);
+
    color: var(--color-text-secondary);
+
    margin-top: 0.25rem;
+
  }
+
  .repo-name {
+
    font: var(--txt-body-s-semibold);
+
    color: var(--color-text-secondary);
+
  }
+
  .size {
+
    font: var(--txt-body-m-mono);
+
    color: var(--color-text-secondary);
+
    white-space: nowrap;
+
  }
+
  .empty {
+
    padding: 3rem 1.5rem;
+
    text-align: center;
+
    color: var(--color-text-secondary);
+
  }
+
</style>
+

+
<div class="modal">
+
  <div class="header">
+
    <span class="title">Seeded artifacts</span>
+
    <Button variant="naked" onclick={hide}>
+
      <span style:color="var(--color-text-tertiary)">
+
        <Icon name="close" />
+
      </span>
+
    </Button>
+
  </div>
+

+
  <div class="summary">
+
    <span>
+
      {artifacts.length}
+
      {artifacts.length === 1 ? "artifact" : "artifacts"} seeded
+
    </span>
+
    <span class="size">{formatBytes(totalBytes)} on disk</span>
+
  </div>
+

+
  <ScrollArea style="flex: 1; min-height: 0;">
+
    {#if loading}
+
      <div class="empty">Loading…</div>
+
    {:else if artifacts.length === 0}
+
      <div class="empty">
+
        You're not seeding any artifacts. Publish or seed one from a release to
+
        see it here.
+
      </div>
+
    {:else}
+
      <div class="list">
+
        {#each artifacts as artifact (artifact.cid)}
+
          <div class="row">
+
            <div>
+
              <div class="name">{artifact.name}</div>
+
              <div class="meta">
+
                <span class="repo-name">
+
                  {artifact.repoName === "" ? artifact.rid : artifact.repoName}
+
                </span>
+
                <span>·</span>
+
                <Id id={artifact.cid} clipboard={artifact.cid} />
+
                <span>·</span>
+
                <span>{artifact.kind}</span>
+
              </div>
+
            </div>
+
            <div class="size">{formatBytes(artifact.sizeBytes)}</div>
+
            <Button
+
              variant="outline"
+
              onclick={() => unseed(artifact)}
+
              disabled={busy[`${artifact.rid}/${artifact.cid}`]}>
+
              Unseed
+
            </Button>
+
          </div>
+
        {/each}
+
      </div>
+
    {/if}
+
  </ScrollArea>
+
</div>
modified src/modals/Settings.svelte
@@ -1,8 +1,9 @@
<script lang="ts">
-
  import { hide } from "@app/lib/modal";
+
  import { hide, show } from "@app/lib/modal";
  import { updateChecker } from "@app/lib/updateChecker.svelte";

  import AnnounceSwitch from "@app/components/AnnounceSwitch.svelte";
+
  import AutoSeedSwitch from "@app/components/AutoSeedSwitch.svelte";
  import BadgeCounterSwitch from "@app/components/BadgeCounterSwitch.svelte";
  import Button from "@app/components/Button.svelte";
  import CodeFontSwitch from "@app/components/CodeFontSwitch.svelte";
@@ -11,6 +12,7 @@
  import Icon from "@app/components/Icon.svelte";
  import ThemeSwitch from "@app/components/ThemeSwitch.svelte";
  import UpdateSwitch from "@app/components/UpdateSwitch.svelte";
+
  import SeedingSettings from "@app/modals/SeedingSettings.svelte";
</script>

<style>
@@ -113,6 +115,28 @@
    </div>
    <div class="row">
      <div class="row-label">
+
        <span class="row-title">Auto-seed artifacts</span>
+
        <span class="row-description">
+
          Serve artifacts you publish over iroh automatically
+
        </span>
+
      </div>
+
      <AutoSeedSwitch />
+
    </div>
+
    <div class="row">
+
      <div class="row-label">
+
        <span class="row-title">Seeded artifacts</span>
+
        <span class="row-description">
+
          See what you're sharing over iroh and free up disk space
+
        </span>
+
      </div>
+
      <Button
+
        variant="outline"
+
        onclick={() => show({ component: SeedingSettings, props: {} })}>
+
        Manage
+
      </Button>
+
    </div>
+
    <div class="row">
+
      <div class="row-label">
        <span class="row-title">Code font</span>
        <span class="row-description">Use a monospace font in code views</span>
      </div>
added src/views/repo/NewRelease.svelte
@@ -0,0 +1,358 @@
+
<script lang="ts">
+
  // Modal-like create flow used inline on the Releases list view.
+
  // Lifecycle:
+
  //   1. user enters a commit/tag OID
+
  //   2. user picks files (or drops them onto the window)
+
  //   3. for each file, compute CID locally so the user sees what
+
  //      they're about to publish
+
  //   4. submit: create_or_open_release, then add_artifact per file,
+
  //      and seed_artifact if the auto-seed setting is on
+

+
  import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
+
  import type { Commit } from "@bindings/repo/Commit";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
+
  import type { Tag } from "@bindings/repo/Tag";
+

+
  import { onMount } from "svelte";
+

+
  import { invoke } from "@app/lib/invoke";
+
  import * as router from "@app/lib/router";
+

+
  import CommitPicker from "@app/components/CommitPicker.svelte";
+

+
  interface Props {
+
    repo: RepoInfo;
+
    onCancel: () => void;
+
  }
+

+
  const { repo, onCancel }: Props = $props();
+

+
  type StagedFile = {
+
    path: string;
+
    name: string;
+
    cid?: string;
+
    error?: string;
+
    computing: boolean;
+
  };
+

+
  let oid = $state("");
+
  // Selected tag refname. "" means "no tag" — the release is keyed by the
+
  // commit OID alone. Annotated tags also contribute their tag OID to the
+
  // COB; lightweight tags only set the commit OID.
+
  let selectedTagName = $state("");
+
  const files = $state<StagedFile[]>([]);
+
  let submitting = $state(false);
+
  let autoSeed = $state(true);
+
  let submitError: string | undefined = $state();
+
  // Recent commits offered as autocomplete suggestions for the OID input.
+
  let commits = $state<Commit[]>([]);
+
  // Tags listed for the repo, surfaced as the primary picker.
+
  let tags = $state<Tag[]>([]);
+

+
  // The annotated-tag OID we'll record alongside the commit, if a tag is
+
  // selected and it's annotated. Lightweight tags resolve to a commit OID
+
  // only and don't contribute a separate tag OID.
+
  const selectedTag = $derived(tags.find(t => t.name === selectedTagName));
+
  const tagOid = $derived(selectedTag?.annotated ? selectedTag.oid : undefined);
+

+
  void invoke<boolean>("get_auto_seed_artifacts").then(v => {
+
    autoSeed = v;
+
  });
+

+
  onMount(async () => {
+
    try {
+
      const [commitsResult, tagsResult] = await Promise.all([
+
        invoke<PaginatedQuery<Commit[]>>("list_repo_commits", {
+
          rid: repo.rid,
+
          skip: 0,
+
          take: 50,
+
        }),
+
        invoke<Tag[]>("list_tags", { rid: repo.rid }),
+
      ]);
+
      commits = commitsResult.content;
+
      tags = tagsResult;
+
    } catch (err) {
+
      console.error("loading commits/tags failed", err);
+
    }
+
  });
+

+
  function onSelectTag(name: string) {
+
    selectedTagName = name;
+
    if (name === "") return;
+
    const tag = tags.find(t => t.name === name);
+
    if (tag) {
+
      // Auto-fill the commit OID from the tag's target. The user can still
+
      // override it manually — and doing so clears the tag selection so we
+
      // don't end up writing a tag OID that doesn't match the commit.
+
      oid = tag.commit;
+
    }
+
  }
+

+
  function onCommitInput(value: string) {
+
    oid = value;
+
    // If the typed value no longer matches the selected tag's commit, the
+
    // tag and commit are out of sync — clear the tag.
+
    if (selectedTag && selectedTag.commit !== value) {
+
      selectedTagName = "";
+
    }
+
  }
+

+
  async function pickFiles() {
+
    const picked = await invoke<string[]>("pick_artifact_files");
+
    for (const p of picked) {
+
      await stageFile(p);
+
    }
+
  }
+

+
  async function pickDirectory() {
+
    const dir = await invoke<string | null>("pick_artifact_directory");
+
    if (dir) await stageFile(dir);
+
  }
+

+
  async function stageFile(path: string) {
+
    const baseName = path.split(/[\\/]/).filter(Boolean).pop() ?? path;
+
    const idx = files.length;
+
    files.push({
+
      path,
+
      name: baseName,
+
      computing: true,
+
    });
+
    // After push, files[idx] returns Svelte 5's reactive proxy view of the
+
    // entry — write through that handle, not a local reference, otherwise
+
    // mutations don't trigger re-renders and the row spins forever.
+
    try {
+
      files[idx].cid = await invoke<string>("compute_artifact_cid", { path });
+
    } catch (err) {
+
      files[idx].error = String(err);
+
    } finally {
+
      files[idx].computing = false;
+
    }
+
  }
+

+
  function remove(index: number) {
+
    files.splice(index, 1);
+
  }
+

+
  async function submit() {
+
    if (!oid.trim()) {
+
      submitError = "Commit OID is required.";
+
      return;
+
    }
+
    if (files.length === 0) {
+
      submitError = "Add at least one artifact.";
+
      return;
+
    }
+
    submitError = undefined;
+
    submitting = true;
+
    try {
+
      const releaseId = await invoke<string>("create_or_open_release", {
+
        rid: repo.rid,
+
        oid: oid.trim(),
+
        tag: tagOid,
+
      });
+
      for (const f of files) {
+
        if (!f.cid) continue;
+
        await invoke("add_artifact", {
+
          rid: repo.rid,
+
          releaseId,
+
          cid: f.cid,
+
          name: f.name,
+
        });
+
        if (autoSeed) {
+
          try {
+
            await invoke("seed_artifact", {
+
              rid: repo.rid,
+
              releaseId,
+
              cid: f.cid,
+
              sourcePath: f.path,
+
            });
+
          } catch (err) {
+
            // Don't block release creation if a single seed fails — the
+
            // artifact is still recorded on the COB and the user can
+
            // retry seeding from the detail view.
+
            console.error("seed failed for", f.cid, err);
+
          }
+
        }
+
      }
+
      await router.push({
+
        resource: "repo.release",
+
        rid: repo.rid,
+
        release: releaseId,
+
      });
+
    } catch (err) {
+
      submitError = String(err);
+
    } finally {
+
      submitting = false;
+
    }
+
  }
+
</script>
+

+
<style>
+
  .form {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1rem;
+
    padding: 1rem;
+
    background-color: var(--color-surface-1);
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
  }
+
  .field {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.25rem;
+
  }
+
  label,
+
  .field-label {
+
    font: var(--txt-body-s-semibold);
+
    color: var(--color-text-secondary);
+
  }
+
  input[type="text"],
+
  select {
+
    padding: 0.4rem 0.5rem;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    background-color: var(--color-surface-canvas);
+
    font: var(--txt-body-m-regular);
+
  }
+
  .files {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.25rem;
+
  }
+
  .file-row {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.25rem;
+
    padding: 0.4rem 0.5rem;
+
    background-color: var(--color-surface-canvas);
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
  }
+
  .file-row .top {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
  }
+
  .file-row .path {
+
    font: var(--txt-body-s-mono);
+
    color: var(--color-text-secondary);
+
    flex: 1;
+
    white-space: nowrap;
+
    overflow: hidden;
+
    text-overflow: ellipsis;
+
  }
+
  .file-row .cid {
+
    font: var(--txt-body-s-mono);
+
    color: var(--color-text-secondary);
+
    word-break: break-all;
+
  }
+
  .file-row input.name {
+
    flex: 0 0 12rem;
+
  }
+
  .actions {
+
    display: flex;
+
    gap: 0.5rem;
+
  }
+
  button {
+
    padding: 0.4rem 0.75rem;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    background-color: var(--color-surface-subtle);
+
    cursor: pointer;
+
  }
+
  button.primary {
+
    background-color: var(--color-fill-accent);
+
    color: var(--color-text-inverse);
+
  }
+
  .error {
+
    color: var(--color-fill-error);
+
    font: var(--txt-body-s-regular);
+
  }
+
  .toggle {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
  }
+
</style>
+

+
<div class="form">
+
  <div class="field">
+
    <label for="release-tag">Tag</label>
+
    <select
+
      id="release-tag"
+
      value={selectedTagName}
+
      onchange={e => onSelectTag((e.currentTarget as HTMLSelectElement).value)}
+
      disabled={submitting}>
+
      <option value="">No tag — release a bare commit</option>
+
      {#each tags as tag (tag.name)}
+
        <option value={tag.name}>
+
          {tag.name}
+
          {tag.annotated ? "(annotated)" : ""}
+
          → {tag.commit.slice(0, 7)}
+
        </option>
+
      {/each}
+
    </select>
+
  </div>
+

+
  <div class="field">
+
    <span class="field-label">Commit</span>
+
    <CommitPicker
+
      {commits}
+
      value={oid}
+
      disabled={submitting}
+
      onSelect={onCommitInput} />
+
  </div>
+

+
  <div class="field">
+
    <span class="field-label">Artifacts</span>
+
    <div class="files">
+
      {#each files as file, i (file.path)}
+
        <div class="file-row">
+
          <div class="top">
+
            <input
+
              class="name"
+
              type="text"
+
              bind:value={file.name}
+
              disabled={submitting} />
+
            <span class="path" title={file.path}>{file.path}</span>
+
            <button onclick={() => remove(i)} disabled={submitting}>×</button>
+
          </div>
+
          {#if file.computing}
+
            <span class="cid">computing…</span>
+
          {:else if file.error}
+
            <span class="error">{file.error}</span>
+
          {:else if file.cid}
+
            <span class="cid">{file.cid}</span>
+
          {/if}
+
        </div>
+
      {/each}
+
    </div>
+
    <div class="actions" style:margin-top="0.5rem">
+
      <button onclick={pickFiles} disabled={submitting}>Add files…</button>
+
      <button onclick={pickDirectory} disabled={submitting}>
+
        Add directory…
+
      </button>
+
    </div>
+
  </div>
+

+
  <div class="toggle">
+
    <input
+
      id="auto-seed"
+
      type="checkbox"
+
      bind:checked={autoSeed}
+
      disabled={submitting} />
+
    <label for="auto-seed">Seed over iroh after publishing</label>
+
  </div>
+

+
  {#if submitError}
+
    <div class="error">{submitError}</div>
+
  {/if}
+

+
  <div class="actions" style:justify-content="flex-end">
+
    <button onclick={onCancel} disabled={submitting}>Cancel</button>
+
    <button class="primary" onclick={submit} disabled={submitting}>
+
      {submitting ? "Publishing…" : "Publish release"}
+
    </button>
+
  </div>
+
</div>
added src/views/repo/Release.svelte
@@ -0,0 +1,1039 @@
+
<script lang="ts">
+
  import type { ReleaseFilter } from "@app/views/repo/router";
+
  import type { Artifact } from "@bindings/cob/release/Artifact";
+
  import type { Release } from "@bindings/cob/release/Release";
+
  import type { Config } from "@bindings/config/Config";
+
  import type { Commit } from "@bindings/repo/Commit";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
+

+
  import { listen } from "@tauri-apps/api/event";
+
  import { onDestroy, onMount } from "svelte";
+

+
  import { invoke, InvokeError } from "@app/lib/invoke";
+
  import { isDelegateOrAuthor } from "@app/lib/roles";
+
  import * as router from "@app/lib/router";
+
  import {
+
    absoluteTimestamp,
+
    authorForNodeId,
+
    formatTimestamp,
+
    publicKeyFromDid,
+
  } from "@app/lib/utils";
+

+
  import Icon from "@app/components/Icon.svelte";
+
  import Id from "@app/components/Id.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";
+
  import ScrollArea from "@app/components/ScrollArea.svelte";
+
  import Topbar from "@app/components/Topbar.svelte";
+

+
  import Layout from "./Layout.svelte";
+

+
  interface Props {
+
    repo: RepoInfo;
+
    release: Release;
+
    config: Config;
+
    filter: ReleaseFilter;
+
  }
+

+
  const { repo, release: releaseProp, config, filter }: Props = $props();
+

+
  let release = $state(releaseProp);
+
  $effect(() => {
+
    release = releaseProp;
+
  });
+

+
  // Resolve the released commit for the description panel. Lookup is best
+
  // effort; if the commit was garbage collected we just hide the panel.
+
  let commit = $state<Commit | undefined>(undefined);
+
  $effect(() => {
+
    const oid = release.oid;
+
    void invoke<Commit>("repo_commit", { rid: repo.rid, sha: oid })
+
      .then(c => {
+
        commit = c;
+
      })
+
      .catch(() => {
+
        commit = undefined;
+
      });
+
  });
+

+
  async function refresh() {
+
    const next = await invoke<Release | null>("release_by_id", {
+
      rid: repo.rid,
+
      releaseId: release.id,
+
    });
+
    if (next) release = next;
+
  }
+

+
  const delegateDids = $derived(repo.delegates.map(d => d.did));
+

+
  function canEditMetadata(artifact: Artifact): boolean {
+
    return (
+
      isDelegateOrAuthor(
+
        config.publicKey,
+
        delegateDids,
+
        artifact.author.did,
+
      ) === true
+
    );
+
  }
+

+
  // Hide the Attest button for the artifact's own author — attestation
+
  // is a vouching signal from someone other than the author, and a
+
  // self-attestation is a no-op in the COB. Delegates who happen to also
+
  // be the author follow the same rule.
+
  function isOwnArtifact(artifact: Artifact): boolean {
+
    return publicKeyFromDid(artifact.author.did) === config.publicKey;
+
  }
+

+
  type PeerRole = "author" | "delegate" | "other";
+

+
  // Classify a peer DID relative to a specific artifact so the UI can
+
  // visually rank attestations / locations / redactions by trust. The
+
  // artifact author wins over delegate if both apply.
+
  function peerRole(did: string, artifact: Artifact): PeerRole {
+
    if (did === artifact.author.did) return "author";
+
    if (delegateDids.includes(did)) return "delegate";
+
    return "other";
+
  }
+

+
  // Redactions written by the artifact's author or a repo delegate are
+
  // treated as authoritative — we blur the artifact body and surface the
+
  // reason so users can still read why it was withdrawn.
+
  function trustedRedactions(artifact: Artifact) {
+
    return artifact.redactions.filter(
+
      r => peerRole(r.peer.did, artifact) !== "other",
+
    );
+
  }
+

+
  // Reveal toggles so the locations/attestations sections stay tucked
+
  // away by default but can be expanded per artifact.
+
  const revealLocations = $state<Record<string, boolean>>({});
+
  const revealAttestations = $state<Record<string, boolean>>({});
+
  // Per-artifact redact form: { reason } when open, undefined when closed.
+
  // Inline because window.prompt is a no-op in Tauri's WebKit webview, so
+
  // the dialog-based flow silently did nothing.
+
  const redactDraft = $state<Record<string, { reason: string } | undefined>>(
+
    {},
+
  );
+

+
  const draft = $state<Record<string, { key: string; value: string }>>({});
+
  $effect(() => {
+
    for (const a of release.artifacts) {
+
      if (!draft[a.cid]) draft[a.cid] = { key: "", value: "" };
+
    }
+
  });
+

+
  const localShared = $state<Record<string, boolean>>({});
+
  const localAvailable = $state<Record<string, boolean>>({});
+
  const busy = $state<Record<string, boolean>>({});
+
  // Per-artifact error messages displayed inline under the action row.
+
  // Cleared when the user retries the same artifact's action.
+
  const actionErrors = $state<Record<string, string | undefined>>({});
+

+
  // Translate a backend error into something a user can act on. Falls
+
  // through to the raw message for codes we haven't styled yet.
+
  function describeError(err: unknown): string {
+
    if (err instanceof InvokeError) {
+
      if (err.code === "ArtifactError.CidMismatch") {
+
        return `${err.message}. Pick the original file that produced this CID.`;
+
      }
+
      if (err.code === "ArtifactError.NoLocations") {
+
        return "This artifact has no advertised locations to fetch from.";
+
      }
+
      if (err.code === "ArtifactError.NoProviders") {
+
        return "No iroh providers are currently reachable for this artifact.";
+
      }
+
      if (err.code === "ArtifactError.AllTransportsFailed") {
+
        return err.message;
+
      }
+
      return err.message;
+
    }
+
    return String(err);
+
  }
+
  const progress = $state<
+
    Record<string, { stage: string; bytes?: number } | undefined>
+
  >({});
+

+
  type ProgressEvent = { cid: string; stage: string; bytes?: number };
+
  let unlistenProgress: (() => void) | undefined;
+

+
  onMount(async () => {
+
    unlistenProgress = await listen<ProgressEvent>("artifact_progress", e => {
+
      const { cid, stage, bytes } = e.payload;
+
      if (stage === "done") {
+
        progress[cid] = undefined;
+
        // Refresh the local-availability pill — a finished download means
+
        // the bytes are now in the store even if we never seeded.
+
        void refreshAvailability(cid);
+
      } else {
+
        progress[cid] = { stage, bytes };
+
      }
+
    });
+
    for (const a of release.artifacts) {
+
      void refreshAvailability(a.cid);
+
    }
+
  });
+

+
  onDestroy(() => {
+
    if (unlistenProgress) unlistenProgress();
+
  });
+

+
  async function refreshAvailability(cid: string) {
+
    try {
+
      localAvailable[cid] = await invoke<boolean>("is_seeding", { cid });
+
    } catch {
+
      localAvailable[cid] = false;
+
    }
+
  }
+

+
  // Strict — only reflect *this* device's state. Falling back to the
+
  // DTO's flag used to flicker the wrong answer for the "seeded on
+
  // another machine" case, where the COB says we shared an iroh URL
+
  // but the bytes aren't in this process's store.
+
  function isShared(a: Artifact): boolean {
+
    return localShared[a.cid] ?? a.sharedFromHere;
+
  }
+

+
  function isAvailableLocally(a: Artifact): boolean {
+
    return localAvailable[a.cid] === true;
+
  }
+

+
  async function seed(artifact: Artifact) {
+
    let path: string | null;
+
    if (artifact.kind === "collection") {
+
      path = await invoke<string | null>("pick_artifact_directory");
+
    } else {
+
      const files = await invoke<string[]>("pick_artifact_files");
+
      path = files[0] ?? null;
+
    }
+
    if (!path) return;
+
    busy[artifact.cid] = true;
+
    actionErrors[artifact.cid] = undefined;
+
    try {
+
      await invoke("seed_artifact", {
+
        rid: repo.rid,
+
        releaseId: release.id,
+
        cid: artifact.cid,
+
        sourcePath: path,
+
      });
+
      localShared[artifact.cid] = true;
+
      localAvailable[artifact.cid] = true;
+
    } catch (err) {
+
      console.error("seed failed", err);
+
      actionErrors[artifact.cid] = describeError(err);
+
    } finally {
+
      busy[artifact.cid] = false;
+
    }
+
  }
+

+
  async function unseed(artifact: Artifact) {
+
    busy[artifact.cid] = true;
+
    actionErrors[artifact.cid] = undefined;
+
    try {
+
      await invoke("unseed_artifact", {
+
        rid: repo.rid,
+
        releaseId: release.id,
+
        cid: artifact.cid,
+
      });
+
      localShared[artifact.cid] = false;
+
      void refreshAvailability(artifact.cid);
+
    } catch (err) {
+
      console.error("unseed failed", err);
+
      actionErrors[artifact.cid] = describeError(err);
+
    } finally {
+
      busy[artifact.cid] = false;
+
    }
+
  }
+

+
  async function download(artifact: Artifact) {
+
    const isCollection = artifact.kind === "collection";
+
    const dest = isCollection
+
      ? await invoke<string | null>("pick_artifact_directory")
+
      : await invoke<string | null>("pick_artifact_save_path", {
+
          suggestedName: artifact.name,
+
        });
+
    if (!dest) return;
+

+
    busy[artifact.cid] = true;
+
    actionErrors[artifact.cid] = undefined;
+
    try {
+
      await invoke("download_artifact", {
+
        rid: repo.rid,
+
        releaseId: release.id,
+
        cid: artifact.cid,
+
        dest,
+
      });
+
    } catch (err) {
+
      console.error("download failed", err);
+
      actionErrors[artifact.cid] = describeError(err);
+
    } finally {
+
      busy[artifact.cid] = false;
+
    }
+
  }
+

+
  async function attest(artifact: Artifact) {
+
    busy[artifact.cid] = true;
+
    actionErrors[artifact.cid] = undefined;
+
    try {
+
      await invoke("attest_artifact", {
+
        rid: repo.rid,
+
        releaseId: release.id,
+
        cid: artifact.cid,
+
      });
+
      await refresh();
+
    } catch (err) {
+
      console.error("attest failed", err);
+
      actionErrors[artifact.cid] = describeError(err);
+
    } finally {
+
      busy[artifact.cid] = false;
+
    }
+
  }
+

+
  function openRedact(artifact: Artifact) {
+
    redactDraft[artifact.cid] = { reason: "" };
+
  }
+

+
  function cancelRedact(artifact: Artifact) {
+
    redactDraft[artifact.cid] = undefined;
+
  }
+

+
  async function confirmRedact(artifact: Artifact) {
+
    const draft = redactDraft[artifact.cid];
+
    if (!draft) return;
+
    const reason = draft.reason.trim();
+
    if (!reason) {
+
      actionErrors[artifact.cid] = "Please provide a reason for the redaction.";
+
      return;
+
    }
+
    busy[artifact.cid] = true;
+
    actionErrors[artifact.cid] = undefined;
+
    try {
+
      await invoke("redact_artifact", {
+
        rid: repo.rid,
+
        releaseId: release.id,
+
        cid: artifact.cid,
+
        reason,
+
      });
+
      redactDraft[artifact.cid] = undefined;
+
      await refresh();
+
    } catch (err) {
+
      console.error("redact failed", err);
+
      actionErrors[artifact.cid] = describeError(err);
+
    } finally {
+
      busy[artifact.cid] = false;
+
    }
+
  }
+

+
  async function setMetadata(artifact: Artifact, key: string, raw: string) {
+
    if (!key.trim()) return;
+
    let value: unknown;
+
    try {
+
      value = JSON.parse(raw);
+
    } catch {
+
      value = raw;
+
    }
+
    busy[artifact.cid] = true;
+
    try {
+
      await invoke("set_metadata", {
+
        rid: repo.rid,
+
        releaseId: release.id,
+
        cid: artifact.cid,
+
        key,
+
        value,
+
      });
+
      draft[artifact.cid] = { key: "", value: "" };
+
      await refresh();
+
    } catch (err) {
+
      console.error("set_metadata failed", err);
+
    } finally {
+
      busy[artifact.cid] = false;
+
    }
+
  }
+

+
  async function removeMetadata(artifact: Artifact, key: string) {
+
    busy[artifact.cid] = true;
+
    try {
+
      await invoke("remove_metadata", {
+
        rid: repo.rid,
+
        releaseId: release.id,
+
        cid: artifact.cid,
+
        key,
+
      });
+
      await refresh();
+
    } catch (err) {
+
      console.error("remove_metadata failed", err);
+
    } finally {
+
      busy[artifact.cid] = false;
+
    }
+
  }
+

+
  function progressText(cid: string): string {
+
    const p = progress[cid];
+
    if (!p) return "";
+
    if (p.stage === "downloading" && p.bytes !== undefined) {
+
      const mb = (p.bytes / (1024 * 1024)).toFixed(1);
+
      return `downloading ${mb} MiB`;
+
    }
+
    return p.stage;
+
  }
+
</script>
+

+
<style>
+
  .page {
+
    display: flex;
+
    flex-direction: column;
+
    height: 100%;
+
  }
+
  .breadcrumb {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.375rem;
+
  }
+
  .breadcrumb-link {
+
    cursor: pointer;
+
    background: none;
+
    border: none;
+
    padding: 0;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
+
  }
+
  .breadcrumb-link:hover {
+
    color: var(--color-text-primary);
+
  }
+
  .header {
+
    padding: 1.25rem 1.5rem;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.75rem;
+
  }
+
  .header h1 {
+
    margin: 0;
+
    font: var(--txt-body-l-semibold);
+
    word-break: break-word;
+
  }
+
  .header .meta {
+
    display: flex;
+
    flex-wrap: wrap;
+
    align-items: center;
+
    gap: 0.5rem;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
+
  }
+
  .commit-panel {
+
    background-color: var(--color-surface-canvas);
+
    border-radius: var(--border-radius-sm);
+
    padding: 0.75rem 1rem;
+
    font: var(--txt-body-m-regular);
+
  }
+
  .commit-summary {
+
    font: var(--txt-body-m-semibold);
+
    color: var(--color-text-primary);
+
    margin-bottom: 0.25rem;
+
  }
+
  .commit-body {
+
    white-space: pre-wrap;
+
    color: var(--color-text-secondary);
+
    font: var(--txt-body-s-mono);
+
  }
+
  .artifact {
+
    padding: 1rem 1.5rem;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
    background-color: var(--color-surface-1);
+
  }
+
  .artifact-row {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.75rem;
+
    flex-wrap: wrap;
+
  }
+
  .name {
+
    font: var(--txt-body-m-semibold);
+
  }
+
  .kind {
+
    font: var(--txt-body-s-regular);
+
    color: var(--color-text-secondary);
+
  }
+
  .pill {
+
    display: inline-flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    height: 1.5rem;
+
    padding: 0 0.5rem;
+
    border-radius: var(--border-radius-sm);
+
    border: 1px solid var(--color-border-subtle);
+
    font: var(--txt-body-s-regular);
+
    color: var(--color-text-secondary);
+
  }
+
  .pill.available {
+
    background-color: var(--color-feedback-success-bg);
+
    border-color: var(--color-feedback-success-border);
+
    color: var(--color-feedback-success-text);
+
  }
+
  .pill.other-device {
+
    background-color: var(--color-feedback-warning-bg);
+
    border-color: var(--color-feedback-warning-border);
+
    color: var(--color-feedback-warning-text);
+
  }
+
  .actions {
+
    margin-left: auto;
+
    display: flex;
+
    gap: 0.25rem;
+
  }
+
  .actions button {
+
    padding: 0.25rem 0.5rem;
+
    background-color: var(--color-surface-subtle);
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    cursor: pointer;
+
    font: var(--txt-body-s-regular);
+
  }
+
  .actions button.danger {
+
    color: var(--color-fill-error);
+
  }
+
  .actions button:disabled {
+
    opacity: 0.5;
+
    cursor: default;
+
  }
+
  .meta-line {
+
    display: flex;
+
    gap: 1rem;
+
    margin-top: 0.5rem;
+
    font: var(--txt-body-s-regular);
+
    color: var(--color-text-secondary);
+
  }
+
  .metadata {
+
    display: grid;
+
    grid-template-columns: auto 1fr;
+
    column-gap: 0.5rem;
+
    row-gap: 0.125rem;
+
    margin: 0.5rem 0 0;
+
    font: var(--txt-body-s-regular);
+
  }
+
  .metadata dt {
+
    font-family: var(--font-family-mono);
+
    color: var(--color-text-secondary);
+
  }
+
  .metadata dd {
+
    margin: 0;
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
  }
+
  .metadata .value {
+
    word-break: break-word;
+
  }
+
  .remove-meta {
+
    padding: 0 0.25rem;
+
    background: transparent;
+
    border: none;
+
    color: var(--color-text-secondary);
+
    cursor: pointer;
+
    line-height: 1;
+
  }
+
  .remove-meta:disabled {
+
    opacity: 0.5;
+
    cursor: default;
+
  }
+
  .add-meta {
+
    display: flex;
+
    gap: 0.25rem;
+
    margin-top: 0.5rem;
+
  }
+
  .add-meta input {
+
    padding: 0.25rem 0.5rem;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    font: var(--txt-body-s-regular);
+
  }
+
  .add-meta input:first-child {
+
    font-family: var(--font-family-mono);
+
    width: 8rem;
+
  }
+
  .add-meta input:nth-child(2) {
+
    flex: 1;
+
  }
+
  .add-meta button {
+
    padding: 0.25rem 0.5rem;
+
    background-color: var(--color-surface-subtle);
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    cursor: pointer;
+
    font: var(--txt-body-s-regular);
+
  }
+
  .add-meta button:disabled {
+
    opacity: 0.5;
+
    cursor: default;
+
  }
+
  .progress {
+
    margin-top: 0.5rem;
+
    font: var(--txt-body-s-regular);
+
    color: var(--color-text-secondary);
+
  }
+
  .action-error {
+
    margin-top: 0.5rem;
+
    padding: 0.5rem 0.75rem;
+
    background-color: var(--color-feedback-error-bg);
+
    border: 1px solid var(--color-feedback-error-border);
+
    border-radius: var(--border-radius-sm);
+
    color: var(--color-feedback-error-text);
+
    font: var(--txt-body-s-regular);
+
    display: flex;
+
    align-items: flex-start;
+
    gap: 0.5rem;
+
  }
+
  .action-error .dismiss {
+
    margin-left: auto;
+
    background: none;
+
    border: none;
+
    cursor: pointer;
+
    color: inherit;
+
    line-height: 1;
+
    font-size: 1rem;
+
    padding: 0;
+
  }
+
  .redact-form {
+
    margin-top: 0.5rem;
+
    padding: 0.75rem;
+
    border: 1px solid var(--color-feedback-error-border);
+
    border-radius: var(--border-radius-sm);
+
    background-color: var(--color-feedback-error-bg);
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.5rem;
+
  }
+
  .redact-form-title {
+
    font: var(--txt-body-s-semibold);
+
    color: var(--color-feedback-error-text);
+
  }
+
  .redact-form textarea {
+
    resize: vertical;
+
    min-height: 3rem;
+
    padding: 0.4rem 0.5rem;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    background-color: var(--color-surface-canvas);
+
    font: var(--txt-body-s-regular);
+
  }
+
  .redact-form .row {
+
    display: flex;
+
    gap: 0.5rem;
+
    justify-content: flex-end;
+
  }
+
  .redact-form button {
+
    padding: 0.25rem 0.75rem;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    background-color: var(--color-surface-subtle);
+
    cursor: pointer;
+
    font: var(--txt-body-s-regular);
+
  }
+
  .redact-form button.confirm {
+
    background-color: var(--color-feedback-error-fill);
+
    color: var(--color-text-inverse);
+
    border-color: var(--color-feedback-error-fill);
+
  }
+
  .redact-form button:disabled {
+
    opacity: 0.5;
+
    cursor: default;
+
  }
+
  .empty {
+
    padding: 2rem;
+
    color: var(--color-text-secondary);
+
    text-align: center;
+
  }
+
  .reveal {
+
    background: none;
+
    border: none;
+
    padding: 0;
+
    cursor: pointer;
+
    font: var(--txt-body-s-regular);
+
    color: var(--color-text-secondary);
+
    text-decoration: underline dotted;
+
  }
+
  .reveal:hover {
+
    color: var(--color-text-primary);
+
  }
+
  .peer-list {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.375rem;
+
    margin-top: 0.5rem;
+
    padding: 0.5rem 0.75rem;
+
    background-color: var(--color-surface-canvas);
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
  }
+
  .peer-row {
+
    display: flex;
+
    align-items: flex-start;
+
    flex-wrap: wrap;
+
    gap: 0.5rem;
+
    font: var(--txt-body-s-regular);
+
  }
+
  .role-badge {
+
    display: inline-flex;
+
    align-items: center;
+
    height: 1.25rem;
+
    padding: 0 0.375rem;
+
    border-radius: var(--border-radius-sm);
+
    font: var(--txt-body-s-regular);
+
    border: 1px solid var(--color-border-subtle);
+
    color: var(--color-text-secondary);
+
  }
+
  .role-badge.author {
+
    background-color: var(--color-feedback-success-bg);
+
    border-color: var(--color-feedback-success-border);
+
    color: var(--color-feedback-success-text);
+
  }
+
  .role-badge.delegate {
+
    background-color: var(--color-feedback-warning-bg);
+
    border-color: var(--color-feedback-warning-border);
+
    color: var(--color-feedback-warning-text);
+
  }
+
  .peer-url-list {
+
    margin: 0;
+
    padding-left: 1rem;
+
    width: 100%;
+
    color: var(--color-text-secondary);
+
    word-break: break-all;
+
  }
+
  .reason {
+
    color: var(--color-text-primary);
+
    margin-left: 0.25rem;
+
  }
+
  .redactions {
+
    margin-top: 0.5rem;
+
    padding: 0.5rem 0.75rem;
+
    background-color: var(--color-feedback-error-bg);
+
    border: 1px solid var(--color-feedback-error-border);
+
    border-radius: var(--border-radius-sm);
+
    color: var(--color-feedback-error-text);
+
  }
+
  .redactions-title {
+
    font: var(--txt-body-s-semibold);
+
    margin-bottom: 0.25rem;
+
  }
+
  .redaction-row {
+
    display: flex;
+
    flex-wrap: wrap;
+
    align-items: center;
+
    gap: 0.375rem;
+
    font: var(--txt-body-s-regular);
+
  }
+
  .blur {
+
    filter: blur(4px);
+
    pointer-events: none;
+
    user-select: none;
+
    opacity: 0.6;
+
  }
+
</style>
+

+
<Layout>
+
  <div class="page">
+
    <Topbar>
+
      <div class="breadcrumb">
+
        <Icon name="commit" />
+
        <button
+
          class="breadcrumb-link"
+
          onclick={() =>
+
            router.push({
+
              resource: "repo.releases",
+
              rid: repo.rid,
+
              filter,
+
            })}>
+
          {filter === "delegate" ? "Delegate releases" : "All releases"}
+
        </button>
+
        <Icon name="chevron-right" />
+
        <Id id={release.id} clipboard={release.id} placement="bottom-start" />
+
      </div>
+
    </Topbar>
+

+
    <ScrollArea style="flex: 1; min-height: 0;">
+
      <div class="header">
+
        <h1>
+
          {release.tagName ??
+
            release.commitSummary ??
+
            `Release ${release.oid.slice(0, 7)}`}
+
        </h1>
+
        <div class="meta">
+
          <NodeId {...authorForNodeId(release.creator)} />
+
          released
+
          <Id id={release.id} clipboard={release.id} ariaLabel="Release ID" />
+
          from commit
+
          <Id id={release.oid} clipboard={release.oid} ariaLabel="Commit OID" />
+
          {#if release.tagName}
+
            <span class="pill">{release.tagName}</span>
+
          {/if}
+
          <span title={absoluteTimestamp(release.timestamp * 1000)}>
+
            {formatTimestamp(release.timestamp * 1000)}
+
          </span>
+
        </div>
+
        {#if commit}
+
          <div class="commit-panel">
+
            <div class="commit-summary">{commit.summary}</div>
+
            {#if commit.message.trim() !== commit.summary.trim()}
+
              <div class="commit-body">
+
                {commit.message.slice(commit.summary.length).trim()}
+
              </div>
+
            {/if}
+
          </div>
+
        {/if}
+
      </div>
+

+
      {#each release.artifacts as artifact (artifact.cid)}
+
        {@const trusted = trustedRedactions(artifact)}
+
        <div class="artifact">
+
          {#if trusted.length > 0}
+
            <div class="redactions">
+
              <div class="redactions-title">
+
                Redacted by {trusted.length === 1
+
                  ? "the artifact author or a delegate"
+
                  : "delegates / the artifact author"}
+
              </div>
+
              {#each trusted as redaction (redaction.peer.did)}
+
                <div class="redaction-row">
+
                  <NodeId {...authorForNodeId(redaction.peer)} />
+
                  <span
+
                    class="role-badge {peerRole(redaction.peer.did, artifact)}">
+
                    {peerRole(redaction.peer.did, artifact)}
+
                  </span>
+
                  <span class="reason">{redaction.reason}</span>
+
                </div>
+
              {/each}
+
            </div>
+
          {/if}
+
          <div class:blur={trusted.length > 0}>
+
            <div class="artifact-row">
+
              <span class="name">{artifact.name}</span>
+
              <span class="kind">[{artifact.kind}]</span>
+
              {#if isAvailableLocally(artifact)}
+
                <span class="pill available" title="Available in local store">
+
                  <Icon name="checkmark" />
+
                  Local
+
                </span>
+
              {/if}
+
              {#if !isAvailableLocally(artifact) && artifact.sharedFromOther}
+
                <span
+
                  class="pill other-device"
+
                  title="You advertised this artifact via iroh from a different device (e.g. the CLI). The bytes aren't in this app's store — that peer must be online to serve them.">
+
                  <Icon name="device" />
+
                  Other device
+
                </span>
+
              {/if}
+
              <div class="actions">
+
                <button
+
                  onclick={() => download(artifact)}
+
                  disabled={busy[artifact.cid] ||
+
                    (!isAvailableLocally(artifact) &&
+
                      artifact.locations.length === 0)}
+
                  title={isAvailableLocally(artifact)
+
                    ? "Export the locally-stored copy to disk"
+
                    : artifact.locations.length === 0
+
                      ? "No locations to download from"
+
                      : "Fetch from a peer and write to disk"}>
+
                  {isAvailableLocally(artifact) ? "Save to disk" : "Download"}
+
                </button>
+
                {#if isShared(artifact)}
+
                  <button
+
                    onclick={() => unseed(artifact)}
+
                    disabled={busy[artifact.cid]}>
+
                    Unseed
+
                  </button>
+
                {:else}
+
                  <button
+
                    onclick={() => seed(artifact)}
+
                    disabled={busy[artifact.cid]}>
+
                    Seed
+
                  </button>
+
                {/if}
+
                {#if !isOwnArtifact(artifact)}
+
                  <button
+
                    onclick={() => attest(artifact)}
+
                    disabled={busy[artifact.cid]}>
+
                    Attest
+
                  </button>
+
                {/if}
+
                {#if canEditMetadata(artifact)}
+
                  <button
+
                    class="danger"
+
                    onclick={() => openRedact(artifact)}
+
                    disabled={busy[artifact.cid] ||
+
                      redactDraft[artifact.cid] !== undefined}>
+
                    Redact
+
                  </button>
+
                {/if}
+
              </div>
+
            </div>
+
            <div style:margin-top="0.375rem">
+
              <Id id={artifact.cid} clipboard={artifact.cid} shorten={false} />
+
            </div>
+
            <div class="meta-line">
+
              <button
+
                class="reveal"
+
                onclick={() =>
+
                  (revealAttestations[artifact.cid] =
+
                    !revealAttestations[artifact.cid])}
+
                disabled={artifact.attestations.length === 0}>
+
                {artifact.attestations.length} attestations
+
              </button>
+
              <button
+
                class="reveal"
+
                onclick={() =>
+
                  (revealLocations[artifact.cid] =
+
                    !revealLocations[artifact.cid])}
+
                disabled={artifact.locations.length === 0}>
+
                {artifact.locations.length} locations
+
              </button>
+
              {#if artifact.redactions.length > trusted.length}
+
                <span style:color="var(--color-feedback-error-text)">
+
                  {artifact.redactions.length - trusted.length} other redactions
+
                </span>
+
              {/if}
+
            </div>
+
            {#if revealAttestations[artifact.cid] && artifact.attestations.length > 0}
+
              <div class="peer-list">
+
                {#each artifact.attestations as att (att.did)}
+
                  <div class="peer-row">
+
                    <NodeId {...authorForNodeId(att)} />
+
                    {#if peerRole(att.did, artifact) !== "other"}
+
                      <span class="role-badge {peerRole(att.did, artifact)}">
+
                        {peerRole(att.did, artifact)}
+
                      </span>
+
                    {/if}
+
                  </div>
+
                {/each}
+
              </div>
+
            {/if}
+
            {#if revealLocations[artifact.cid] && artifact.locations.length > 0}
+
              <div class="peer-list">
+
                {#each artifact.locations as loc (loc.peer.did)}
+
                  <div class="peer-row">
+
                    <NodeId {...authorForNodeId(loc.peer)} />
+
                    {#if peerRole(loc.peer.did, artifact) !== "other"}
+
                      <span
+
                        class="role-badge {peerRole(loc.peer.did, artifact)}">
+
                        {peerRole(loc.peer.did, artifact)}
+
                      </span>
+
                    {/if}
+
                    <ul class="peer-url-list">
+
                      {#each loc.urls as url}
+
                        <li>{url}</li>
+
                      {/each}
+
                    </ul>
+
                  </div>
+
                {/each}
+
              </div>
+
            {/if}
+
            {#if artifact.redactions.length > trusted.length}
+
              <div class="peer-list">
+
                {#each artifact.redactions.filter(r => peerRole(r.peer.did, artifact) === "other") as redaction (redaction.peer.did)}
+
                  <div class="peer-row">
+
                    <NodeId {...authorForNodeId(redaction.peer)} />
+
                    <span class="reason">{redaction.reason}</span>
+
                  </div>
+
                {/each}
+
              </div>
+
            {/if}
+
            {#if redactDraft[artifact.cid]}
+
              <form
+
                class="redact-form"
+
                onsubmit={e => {
+
                  e.preventDefault();
+
                  void confirmRedact(artifact);
+
                }}>
+
                <div class="redact-form-title">
+
                  Redact "{artifact.name}"?
+
                </div>
+
                <div style:font="var(--txt-body-s-regular)">
+
                  This records a signed redaction on the release COB. Other
+
                  peers will see it and the artifact will appear blurred for
+
                  delegates and authors. The CID and existing locations stay on
+
                  the COB.
+
                </div>
+
                <textarea
+
                  placeholder="Reason (required)"
+
                  bind:value={redactDraft[artifact.cid]!.reason}
+
                  disabled={busy[artifact.cid]}>
+
                </textarea>
+
                <div class="row">
+
                  <button
+
                    type="button"
+
                    onclick={() => cancelRedact(artifact)}
+
                    disabled={busy[artifact.cid]}>
+
                    Cancel
+
                  </button>
+
                  <button
+
                    type="submit"
+
                    class="confirm"
+
                    disabled={busy[artifact.cid]}>
+
                    {busy[artifact.cid] ? "Redacting…" : "Redact"}
+
                  </button>
+
                </div>
+
              </form>
+
            {/if}
+
            {#if artifact.metadata && Object.keys(artifact.metadata).length > 0}
+
              <dl class="metadata">
+
                {#each Object.entries(artifact.metadata) as [key, value] (key)}
+
                  <dt>{key}</dt>
+
                  <dd>
+
                    <span class="value">
+
                      {typeof value === "string"
+
                        ? value
+
                        : JSON.stringify(value)}
+
                    </span>
+
                    {#if canEditMetadata(artifact)}
+
                      <button
+
                        class="remove-meta"
+
                        title="Remove"
+
                        onclick={() => removeMetadata(artifact, key)}
+
                        disabled={busy[artifact.cid]}>
+
                        ×
+
                      </button>
+
                    {/if}
+
                  </dd>
+
                {/each}
+
              </dl>
+
            {/if}
+
            {#if canEditMetadata(artifact) && draft[artifact.cid]}
+
              <form
+
                class="add-meta"
+
                onsubmit={e => {
+
                  e.preventDefault();
+
                  const d = draft[artifact.cid];
+
                  void setMetadata(artifact, d.key, d.value);
+
                }}>
+
                <input
+
                  type="text"
+
                  placeholder="key"
+
                  bind:value={draft[artifact.cid].key}
+
                  disabled={busy[artifact.cid]} />
+
                <input
+
                  type="text"
+
                  placeholder="value (string or JSON)"
+
                  bind:value={draft[artifact.cid].value}
+
                  disabled={busy[artifact.cid]} />
+
                <button type="submit" disabled={busy[artifact.cid]}>Add</button>
+
              </form>
+
            {/if}
+
            {#if progress[artifact.cid]}
+
              <div class="progress">{progressText(artifact.cid)}</div>
+
            {/if}
+
            {#if actionErrors[artifact.cid]}
+
              <div class="action-error" role="alert">
+
                <Icon name="warning" />
+
                <span>{actionErrors[artifact.cid]}</span>
+
                <button
+
                  class="dismiss"
+
                  title="Dismiss"
+
                  onclick={() => (actionErrors[artifact.cid] = undefined)}>
+
                  ×
+
                </button>
+
              </div>
+
            {/if}
+
          </div>
+
        </div>
+
      {:else}
+
        <div class="empty">No artifacts in this release</div>
+
      {/each}
+
    </ScrollArea>
+
  </div>
+
</Layout>
added src/views/repo/Releases.svelte
@@ -0,0 +1,222 @@
+
<script lang="ts">
+
  import type { ReleaseFilter } from "@app/views/repo/router";
+
  import type { Release } from "@bindings/cob/release/Release";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
+

+
  import fuzzysort from "fuzzysort";
+

+
  import * as router from "@app/lib/router";
+
  import { modifierKey } from "@app/lib/utils";
+

+
  import Button from "@app/components/Button.svelte";
+
  import FuzzySearch from "@app/components/FuzzySearch.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import ReleaseTeaser from "@app/components/ReleaseTeaser.svelte";
+
  import ScrollArea from "@app/components/ScrollArea.svelte";
+
  import Topbar from "@app/components/Topbar.svelte";
+

+
  import Layout from "./Layout.svelte";
+
  import NewRelease from "./NewRelease.svelte";
+

+
  interface Props {
+
    repo: RepoInfo;
+
    releases: Release[];
+
    filter: ReleaseFilter;
+
  }
+

+
  const { repo, releases, filter }: Props = $props();
+

+
  let showNew = $state(false);
+
  let searchInput = $state("");
+
  let showSearch = $state(false);
+

+
  async function setFilter(next: ReleaseFilter) {
+
    await router.push({
+
      resource: "repo.releases",
+
      rid: repo.rid,
+
      filter: next,
+
    });
+
  }
+

+
  const delegateDids = $derived(repo.delegates.map(d => d.did));
+

+
  // A redaction is "trusted" when its peer is the artifact author or a
+
  // repo delegate. The detail view already blurs the artifact in that
+
  // case; here we hide the whole release if every artifact has been
+
  // redacted that way so the list isn't cluttered with withdrawn rows.
+
  function isFullyRedacted(release: (typeof releases)[number]): boolean {
+
    if (release.artifacts.length === 0) return false;
+
    return release.artifacts.every(a =>
+
      a.redactions.some(
+
        r => r.peer.did === a.author.did || delegateDids.includes(r.peer.did),
+
      ),
+
    );
+
  }
+

+
  // Releases without artifacts are placeholders the user hasn't published
+
  // anything to yet — keep them off the list so it shows actual deliverables.
+
  const baseReleases = $derived(
+
    releases.filter(r => r.artifacts.length > 0 && !isFullyRedacted(r)),
+
  );
+

+
  // Pre-compute the delegate set so the counter badges stay in sync with
+
  // whatever the active filter is showing.
+
  const delegateReleases = $derived(
+
    baseReleases.filter(r => delegateDids.includes(r.creator.did)),
+
  );
+

+
  const visibleReleases = $derived(
+
    filter === "delegate" ? delegateReleases : baseReleases,
+
  );
+

+
  const searchable = $derived(
+
    visibleReleases.map(release => ({
+
      release,
+
      creator: release.creator.alias ?? "",
+
      tagName: release.tagName ?? "",
+
      summary: release.commitSummary ?? "",
+
      artifactNames: release.artifacts.map(a => a.name).join(" "),
+
    })),
+
  );
+

+
  const searchResults = $derived(
+
    fuzzysort.go(searchInput, searchable, {
+
      keys: ["release.oid", "tagName", "summary", "creator", "artifactNames"],
+
      threshold: 0.5,
+
      all: true,
+
    }),
+
  );
+
</script>
+

+
<style>
+
  .page {
+
    display: flex;
+
    flex-direction: column;
+
    height: 100%;
+
  }
+
  .topbar-title {
+
    font: var(--txt-body-m-semibold);
+
    color: var(--color-text-secondary);
+
    padding-right: 0.25rem;
+
  }
+
  .list {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1px;
+
    min-height: 100%;
+
  }
+
  .filters {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
  }
+
  .filter {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
+
    padding: 0.25rem 0.5rem;
+
    border-radius: var(--border-radius-sm);
+
    background: none;
+
    border: none;
+
    cursor: pointer;
+
    white-space: nowrap;
+
  }
+
  .filter:hover {
+
    background-color: var(--color-surface-subtle);
+
    color: var(--color-text-primary);
+
  }
+
  .filter.active {
+
    background-color: var(--color-surface-subtle);
+
    color: var(--color-text-primary);
+
  }
+
  .filter-label {
+
    display: none;
+
  }
+
  .filter.active .filter-label {
+
    display: inline;
+
  }
+
  @media (min-width: 1011px) {
+
    .filter-label {
+
      display: inline;
+
    }
+
  }
+
</style>
+

+
<Layout selfScroll>
+
  <div class="page">
+
    <Topbar>
+
      <span class="topbar-title">Releases</span>
+
      <div class="filters">
+
        <button
+
          class="filter"
+
          class:active={filter === "delegate"}
+
          onclick={() => void setFilter("delegate")}
+
          title="Only releases authored by repo delegates">
+
          <Icon name="badge" />
+
          <span class="filter-label">Delegates</span>
+
          <span class="global-counter-badge">{delegateReleases.length}</span>
+
        </button>
+
        <button
+
          class="filter"
+
          class:active={filter === "all"}
+
          onclick={() => void setFilter("all")}
+
          title="Every release published to this repo">
+
          <Icon name="archive" />
+
          <span class="filter-label">All</span>
+
          <span class="global-counter-badge">{baseReleases.length}</span>
+
        </button>
+
      </div>
+
      <div class="global-flex" style:margin-left="auto" style:gap="0.5rem">
+
        <FuzzySearch
+
          hasItems={visibleReleases.length > 0}
+
          placeholder={`Fuzzy filter releases ${modifierKey()} + f`}
+
          bind:show={showSearch}
+
          bind:value={searchInput} />
+
        <Button
+
          variant="secondary"
+
          styleHeight="2rem"
+
          onclick={() => (showNew = !showNew)}
+
          active={showNew}>
+
          <Icon name={showNew ? "close" : "plus"} />
+
          <span class="global-hide-on-small-desktop-down">
+
            {showNew ? "Cancel" : "New release"}
+
          </span>
+
        </Button>
+
      </div>
+
    </Topbar>
+

+
    {#if showNew}
+
      <div style:padding="1rem">
+
        <NewRelease {repo} onCancel={() => (showNew = false)} />
+
      </div>
+
    {/if}
+

+
    <ScrollArea style="height: 100%; min-width: 0;">
+
      <div class="list">
+
        {#each searchResults as result (result.obj.release.id)}
+
          <ReleaseTeaser release={result.obj.release} rid={repo.rid} {filter} />
+
        {/each}
+

+
        {#if searchResults.length === 0}
+
          <div
+
            class="global-flex"
+
            style:flex="1"
+
            style:justify-content="center"
+
            style:align-items="center">
+
            <div class="txt-missing txt-body-m-regular">
+
              {#if visibleReleases.length > 0}
+
                No matching releases
+
              {:else if filter === "delegate" && baseReleases.length > 0}
+
                No releases from delegates yet
+
              {:else}
+
                No releases yet
+
              {/if}
+
            </div>
+
          </div>
+
        {/if}
+
      </div>
+
    </ScrollArea>
+
  </div>
+
</Layout>
modified src/views/repo/router.ts
@@ -6,6 +6,7 @@ import type { Action as PatchAction } from "@bindings/cob/patch/Action";
import type { Patch } from "@bindings/cob/patch/Patch";
import type { Review } from "@bindings/cob/patch/Review";
import type { Revision } from "@bindings/cob/patch/Revision";
+
import type { Release } from "@bindings/cob/release/Release";
import type { Thread } from "@bindings/cob/thread/Thread";
import type { Config } from "@bindings/config/Config";
import type { Diff } from "@bindings/diff/Diff";
@@ -151,6 +152,46 @@ export interface LoadedRepoPatchesRoute {
  };
}

+
// Releases list filter. `delegate` is the default; threaded through to
+
// the release detail route so the breadcrumb can navigate back to the
+
// same filtered list.
+
export type ReleaseFilter = "delegate" | "all";
+

+
export interface RepoReleasesRoute {
+
  resource: "repo.releases";
+
  rid: string;
+
  filter?: ReleaseFilter;
+
}
+

+
export interface LoadedRepoReleasesRoute {
+
  resource: "repo.releases";
+
  params: {
+
    repo: RepoInfo;
+
    releases: Release[];
+
    filter: ReleaseFilter;
+
    sidebarData: SidebarData;
+
  };
+
}
+

+
export interface RepoReleaseRoute {
+
  resource: "repo.release";
+
  rid: string;
+
  release: string;
+
  filter?: ReleaseFilter;
+
}
+

+
export interface LoadedRepoReleaseRoute {
+
  resource: "repo.release";
+
  params: {
+
    repo: RepoInfo;
+
    config: Config;
+
    releases: Release[];
+
    release: Release;
+
    filter: ReleaseFilter;
+
    sidebarData: SidebarData;
+
  };
+
}
+

export type RepoRoute =
  | RepoHomeRoute
  | RepoCommitsRoute
@@ -158,7 +199,9 @@ export type RepoRoute =
  | RepoIssueRoute
  | RepoIssuesRoute
  | RepoPatchRoute
-
  | RepoPatchesRoute;
+
  | RepoPatchesRoute
+
  | RepoReleasesRoute
+
  | RepoReleaseRoute;
export type LoadedRepoRoute =
  | LoadedRepoHomeRoute
  | LoadedRepoCommitsRoute
@@ -166,7 +209,9 @@ export type LoadedRepoRoute =
  | LoadedRepoIssueRoute
  | LoadedRepoIssuesRoute
  | LoadedRepoPatchRoute
-
  | LoadedRepoPatchesRoute;
+
  | LoadedRepoPatchesRoute
+
  | LoadedRepoReleasesRoute
+
  | LoadedRepoReleaseRoute;

export async function loadPatch(
  route: RepoPatchRoute,
@@ -356,6 +401,51 @@ export async function loadIssue(
  };
}

+
export async function loadReleases(
+
  route: RepoReleasesRoute,
+
): Promise<LoadedRepoReleasesRoute> {
+
  const [sidebarData, repo, releases] = await Promise.all([
+
    loadSidebarData(),
+
    invoke<RepoInfo>("repo_by_id", { rid: route.rid }),
+
    invoke<Release[]>("list_releases", { rid: route.rid }),
+
  ]);
+

+
  return {
+
    resource: "repo.releases",
+
    params: { sidebarData, repo, releases, filter: route.filter ?? "delegate" },
+
  };
+
}
+

+
export async function loadRelease(
+
  route: RepoReleaseRoute,
+
): Promise<LoadedRepoReleaseRoute | null> {
+
  const [sidebarData, repo, release, releases] = await Promise.all([
+
    loadSidebarData(),
+
    invoke<RepoInfo>("repo_by_id", { rid: route.rid }),
+
    invoke<Release | null>("release_by_id", {
+
      rid: route.rid,
+
      releaseId: route.release,
+
    }),
+
    invoke<Release[]>("list_releases", { rid: route.rid }),
+
  ]);
+

+
  if (!release) {
+
    return null;
+
  }
+

+
  return {
+
    resource: "repo.release",
+
    params: {
+
      sidebarData,
+
      repo,
+
      config: sidebarData.config,
+
      release,
+
      releases,
+
      filter: route.filter ?? "delegate",
+
    },
+
  };
+
}
+

export async function loadIssues(
  route: RepoIssuesRoute,
): Promise<LoadedRepoIssuesRoute> {
@@ -416,6 +506,21 @@ export function repoRouteToPath(route: RepoRoute): string {
      url += `?${searchParams}`;
    }
    return url;
+
  } else if (route.resource === "repo.releases") {
+
    let url = [...pathSegments, "releases"].join("/");
+
    // Only serialize a non-default filter so the canonical URL stays clean.
+
    if (route.filter && route.filter !== "delegate") {
+
      searchParams.set("filter", route.filter);
+
      url += `?${searchParams}`;
+
    }
+
    return url;
+
  } else if (route.resource === "repo.release") {
+
    let url = [...pathSegments, "releases", route.release].join("/");
+
    if (route.filter && route.filter !== "delegate") {
+
      searchParams.set("filter", route.filter);
+
      url += `?${searchParams}`;
+
    }
+
    return url;
  } else {
    return unreachable(route);
  }
@@ -477,6 +582,17 @@ export function repoUrlToRoute(
      } else {
        return { resource: "repo.patches", rid, status };
      }
+
    } else if (resource === "releases") {
+
      const id = segments.shift();
+
      const filterParam = searchParams.get("filter");
+
      const filter: ReleaseFilter | undefined =
+
        filterParam === "all" || filterParam === "delegate"
+
          ? filterParam
+
          : undefined;
+
      if (id) {
+
        return { resource: "repo.release", rid, release: id, filter };
+
      }
+
      return { resource: "repo.releases", rid, filter };
    } else {
      return null;
    }