commit b1da9f35fe36b6c97b458908a69cc0b6d45a136b
parent 71e0562269049fdef3159f1dddd462a90e343019
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Thu, 10 Sep 2026 14:11:48 +0200
feat: add signed automatic updates
Diffstat:
20 files changed, 1583 insertions(+), 33 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -11,6 +11,7 @@
/downloads/*
!/downloads/.gitkeep
/config/admin-ip-allowlist.local
+/config/update-signing.key
__pycache__/
*.py[cod]
.scratch
diff --git a/Cargo.lock b/Cargo.lock
@@ -3,6 +3,12 @@
version = 4
[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -101,6 +107,12 @@ dependencies = [
]
[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -226,6 +238,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -235,6 +263,15 @@ dependencies = [
]
[[package]]
+name = "crc32fast"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -294,6 +331,17 @@ dependencies = [
]
[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -352,18 +400,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
name = "find-msvc-tools"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
[[package]]
+name = "flate2"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+ "zlib-rs",
+]
+
+[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -548,6 +632,23 @@ dependencies = [
"pin-project-lite",
"smallvec",
"tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
]
[[package]]
@@ -556,13 +657,125 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
+ "base64",
"bytes",
+ "futures-channel",
+ "futures-util",
"http",
"http-body",
"hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
"pin-project-lite",
+ "socket2",
"tokio",
"tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
+
+[[package]]
+name = "icu_properties"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
+
+[[package]]
+name = "icu_provider"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
]
[[package]]
@@ -575,6 +788,12 @@ dependencies = [
]
[[package]]
+name = "ipnet"
+version = "2.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
+
+[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -586,27 +805,44 @@ version = "0.4.29"
dependencies = [
"anyhow",
"axum",
+ "base64",
"bip39",
"chacha20poly1305",
"ed25519-dalek",
+ "flate2",
"getrandom 0.2.17",
"kyn-vdf",
+ "minisign-verify",
"num-bigint",
"num-integer",
"num-traits",
"pbkdf2",
"proptest",
+ "reqwest",
"rusqlite",
"secrecy",
+ "semver",
"serde",
"serde_json",
"sha2",
+ "tar",
"tempfile",
"tokio",
"tower",
]
[[package]]
+name = "js-sys"
+version = "0.3.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "kyn-vdf"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -645,6 +881,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
+name = "litemap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
+
+[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -678,6 +920,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
+name = "minisign-verify"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
name = "mio"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -689,6 +947,23 @@ dependencies = [
]
[[package]]
+name = "native-tls"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -729,6 +1004,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
+name = "openssl"
+version = "0.10.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -801,6 +1119,15 @@ dependencies = [
]
[[package]]
+name = "potential_utf"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -927,6 +1254,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-tls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
name = "rusqlite"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -963,6 +1326,15 @@ dependencies = [
]
[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -987,6 +1359,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1003,6 +1384,29 @@ dependencies = [
]
[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "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 = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1111,6 +1515,12 @@ dependencies = [
]
[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1143,6 +1553,12 @@ dependencies = [
]
[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1175,6 +1591,31 @@ name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tar"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
[[package]]
name = "tempfile"
@@ -1210,6 +1651,16 @@ dependencies = [
]
[[package]]
+name = "tinystr"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
name = "tinyvec"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1253,6 +1704,16 @@ dependencies = [
]
[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1269,6 +1730,24 @@ dependencies = [
]
[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1301,6 +1780,12 @@ dependencies = [
]
[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1338,6 +1823,24 @@ dependencies = [
]
[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1359,6 +1862,15 @@ dependencies = [
]
[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1381,9 +1893,9 @@ checksum = "e59fe57342dd136b22e8c7d1856e276b69e07a8f88153d218d2e54f19991a3eb"
[[package]]
name = "wasm-bindgen"
-version = "0.2.125"
+version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
+checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
dependencies = [
"cfg-if",
"once_cell",
@@ -1393,10 +1905,20 @@ dependencies = [
]
[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.125"
+version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
+checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1404,27 +1926,37 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.125"
+version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
+checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.3",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.125"
+version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
+checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
dependencies = [
"unicode-ident",
]
[[package]]
+name = "web-sys"
+version = "0.3.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1446,6 +1978,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
+name = "writeable"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
+
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
name = "zerocopy"
version = "0.8.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1466,12 +2037,72 @@ dependencies = [
]
[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
+name = "zerotrie"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
+
+[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
@@ -8,6 +8,7 @@ license = "Apache-2.0"
[dependencies]
anyhow = "1.0.98"
axum = "0.8.4"
+base64 = { version = "0.22", optional = true }
bip39 = "2.2.2"
chacha20poly1305 = "0.10.1"
ed25519-dalek = "2.2.0"
@@ -23,8 +24,22 @@ rusqlite = { version = "0.32.1", features = ["bundled"] }
tokio = { version = "1.45.1", features = ["full"] }
kyn-vdf = "=0.1.1"
secrecy = { version = "0.10.3", default-features = false, features = ["serde"] }
+reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"], optional = true }
+semver = { version = "1", optional = true }
+flate2 = { version = "1", optional = true }
+tar = { version = "0.4", optional = true }
+minisign-verify = { version = "0.2.5", optional = true }
[features]
+default = ["cli-updater"]
+cli-updater = [
+ "dep:base64",
+ "dep:flate2",
+ "dep:minisign-verify",
+ "dep:reqwest",
+ "dep:semver",
+ "dep:tar",
+]
e2e = []
fuzzing = []
diff --git a/Dockerfile b/Dockerfile
@@ -70,7 +70,7 @@ RUN set -eux; \
mkdir -p /site/downloads; \
cp /site/downloads.html /site/downloads/index.html; \
sed -i "s|\${IUNA_VERSION}|${version}|g" /site/downloads/index.html; \
- printf '{"tag":"v%s","version":"%s","url":"https://getiuna.org/downloads/"}\n' "$version" "$version" > /site/downloads/latest.json; \
+ if [ ! -f /site/downloads/latest.json ]; then printf '{"tag":"v%s","version":"%s","url":"https://getiuna.org/downloads/"}\n' "$version" "$version" > /site/downloads/latest.json; fi; \
rm -f /site/downloads.html; \
python3 /src/iuna-work/scripts/inject_cloudflare_analytics.py /site
diff --git a/README.md b/README.md
@@ -66,12 +66,22 @@ On macOS and Windows, closing the desktop window keeps the node running from the
system tray. Choose **Open iuna** to reopen the window, or **Quit iuna** to stop the node. On
Windows, a left click on the tray icon also reopens the window directly.
+The desktop app checks for signed updates. When a new release is available, click the version
+badge and choose **Install and restart**. The bundled node is stopped before installation; wallet,
+settings, and chain data are not part of the app bundle and remain in place.
+
Release and deploy with:
```sh
./deployment.sh 0.4.7
```
+Updater artifacts are signed with the private key at `config/update-signing.key` by default. This
+file is ignored by Git and must be backed up separately; losing it prevents existing installations
+from accepting future updates. Set `IUNA_UPDATE_SIGNING_KEY` to use a securely stored copy, and
+`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` when that key is password-protected. The matching public key
+is committed at `config/update-signing.key.pub`.
+
Releases regenerate [`CHANGELOG.md`](CHANGELOG.md) automatically from the full
tagged Git history and commit titles. All new commits must use a Conventional
Commit prefix such as `feat:`, `fix(wallet):`, or `docs:`. Enable the repository
@@ -160,6 +170,18 @@ On Windows PowerShell:
The binary prints a local management URL. Open it and follow setup.
+On supported Linux releases, check or install a signed CLI update with:
+
+```sh
+iuna --version
+iuna update --check
+iuna update
+```
+
+The updater replaces only the running executable. If it is installed in a system-owned directory,
+run the command with suitable permissions or move Iuna to a user-writable binary directory. Restart
+any long-running service after updating.
+
## Optional: Local Docker Testnet
For local P2P and consensus testing, start a six-node testnet with Docker
diff --git a/config/update-signing.key.pub b/config/update-signing.key.pub
@@ -0,0 +1 @@
+dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEEzMEJDQjQ4RDVDQUY1MTMKUldRVDljclZTTXNMbzgvU2tIenpMQjdWbnNhbE9NV1RjZGthQVNYdEhiUkcvdEl1TlZCZVBmYUwK+
\ No newline at end of file
diff --git a/deployment.sh b/deployment.sh
@@ -109,6 +109,27 @@ ensure_tauri_cli() {
fi
}
+update_signing_key() {
+ local key="${IUNA_UPDATE_SIGNING_KEY:-config/update-signing.key}"
+ [ -f "$key" ] || die "missing update signing key: ${key}; restore it from the secure release-key backup"
+ (
+ cd "$(dirname "$key")"
+ printf '%s/%s\n' "$(pwd)" "$(basename "$key")"
+ )
+}
+
+validate_update_public_key() {
+ local configured_key
+ local committed_key
+
+ require_command jq
+ configured_key="$(jq -r '.plugins.updater.pubkey' src-tauri/tauri.conf.json)"
+ committed_key="$(tr -d '\r\n' < config/update-signing.key.pub)"
+ [ -n "$configured_key" ] || die "desktop updater public key is empty"
+ [ "$configured_key" = "$committed_key" ] || \
+ die "src-tauri/tauri.conf.json updater key does not match config/update-signing.key.pub"
+}
+
clear_nsis_installers() {
local nsis_dir="$1"
@@ -348,7 +369,10 @@ build_macos_desktop_if_possible() {
local version="$1"
local artifact="downloads/iuna-v${version}-macos-aarch64-desktop.app.zip"
- [ -f "$artifact" ] && return 0
+ [ -f "$artifact" ] \
+ && [ -f "downloads/iuna-v${version}-macos-aarch64-desktop-update.app.tar.gz" ] \
+ && [ -f "downloads/iuna-v${version}-macos-aarch64-desktop-update.app.tar.gz.sig" ] \
+ && return 0
[ "$(uname -s)" = "Darwin" ] || return 0
is_apple_silicon_macos || die "macOS desktop artifact requires Apple silicon; expected ${artifact}"
@@ -356,40 +380,56 @@ build_macos_desktop_if_possible() {
require_command ditto
require_command rustup
ensure_tauri_cli
+ local signing_key
+ signing_key="$(update_signing_key)"
rustup target add aarch64-apple-darwin
cargo build --release --locked --target aarch64-apple-darwin
mkdir -p src-tauri/binaries downloads
cp target/aarch64-apple-darwin/release/iuna src-tauri/binaries/iuna-sidecar-aarch64-apple-darwin
chmod +x src-tauri/binaries/iuna-sidecar-aarch64-apple-darwin
- (cd src-tauri && cargo tauri build --target aarch64-apple-darwin --bundles app)
+ (cd src-tauri && \
+ TAURI_SIGNING_PRIVATE_KEY="$(cat "$signing_key")" \
+ TAURI_SIGNING_PRIVATE_KEY_PASSWORD="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD-}" \
+ cargo tauri build --target aarch64-apple-darwin --bundles app)
local app="src-tauri/target/aarch64-apple-darwin/release/bundle/macos/iuna.app"
- codesign --force --deep --sign - --options runtime "$app"
+ local updater_archive="${app}.tar.gz"
codesign --verify --deep --strict --verbose=4 "$app"
+ [ -f "$updater_archive" ] || die "missing macOS updater archive: ${updater_archive}"
+ [ -f "${updater_archive}.sig" ] || die "missing macOS updater signature: ${updater_archive}.sig"
ditto -c -k --keepParent "$app" "$artifact"
+ cp "$updater_archive" "downloads/iuna-v${version}-macos-aarch64-desktop-update.app.tar.gz"
+ cp "${updater_archive}.sig" "downloads/iuna-v${version}-macos-aarch64-desktop-update.app.tar.gz.sig"
}
build_windows_desktop_if_possible() {
local version="$1"
local artifact="downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe"
- [ -f "$artifact" ] && return 0
+ [ -f "$artifact" ] && [ -f "${artifact}.sig" ] && return 0
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) ;;
*) return 0 ;;
esac
ensure_tauri_cli
+ local signing_key
+ signing_key="$(update_signing_key)"
cargo build --release --locked
mkdir -p src-tauri/binaries downloads
cp target/release/iuna.exe src-tauri/binaries/iuna-sidecar-x86_64-pc-windows-msvc.exe
local nsis_dir="src-tauri/target/release/bundle/nsis"
clear_nsis_installers "$nsis_dir"
- (cd src-tauri && cargo tauri build --bundles nsis)
+ (cd src-tauri && \
+ TAURI_SIGNING_PRIVATE_KEY="$(cat "$signing_key")" \
+ TAURI_SIGNING_PRIVATE_KEY_PASSWORD="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD-}" \
+ cargo tauri build --bundles nsis)
local installer
installer="$(versioned_nsis_installer "$nsis_dir" "$version")"
cp "$installer" "$artifact"
+ [ -f "${installer}.sig" ] || die "missing Windows updater signature: ${installer}.sig"
+ cp "${installer}.sig" "${artifact}.sig"
}
build_windows_desktop_in_docker_if_possible() {
@@ -397,18 +437,21 @@ build_windows_desktop_in_docker_if_possible() {
local artifact="downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe"
local builder_platform
local builder_arch
+ local signing_key
- [ -f "$artifact" ] && return 0
+ [ -f "$artifact" ] && [ -f "${artifact}.sig" ] && return 0
command -v docker >/dev/null 2>&1 || return 0
builder_platform="$(docker_native_linux_platform)"
builder_arch="${builder_platform#linux/}"
+ signing_key="$(update_signing_key)"
mkdir -p downloads
docker run --rm --pull=always --platform="$builder_platform" \
-e "IUNA_VERSION=${version}" \
-e "HOST_UID=$(id -u)" \
-e "HOST_GID=$(id -g)" \
+ -e "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=${TAURI_SIGNING_PRIVATE_KEY_PASSWORD-}" \
-v iuna-windows-cargo-registry:/usr/local/cargo/registry \
-v iuna-windows-cargo-git:/usr/local/cargo/git \
-v iuna-windows-root-cache:/root/.cache \
@@ -416,10 +459,13 @@ build_windows_desktop_in_docker_if_possible() {
-v "iuna-windows-${builder_arch}-tauri-target:/work/iuna/src-tauri/target" \
-v "$(pwd):/src/iuna:ro" \
-v "$(pwd)/downloads:/out" \
+ -v "${signing_key}:/run/secrets/iuna-update.key:ro" \
rust:1.88-bookworm \
bash -c '
set -euo pipefail
+ export TAURI_SIGNING_PRIVATE_KEY="$(cat /run/secrets/iuna-update.key)"
+
apt-get update
# The Linux-hosted Tauri CLI inspects enabled tray features while preparing
# bundle settings, even when cargo-xwin targets a Windows NSIS installer.
@@ -467,7 +513,9 @@ build_windows_desktop_in_docker_if_possible() {
installer="${nsis_dir}/iuna_${IUNA_VERSION}_x64-setup.exe"
[ -f "$installer" ] || { echo "Windows installer for version ${IUNA_VERSION} was not produced at ${installer}" >&2; exit 1; }
cp "$installer" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe"
- chown "${HOST_UID}:${HOST_GID}" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe"
+ test -f "${installer}.sig" || { echo "missing Windows updater signature: ${installer}.sig" >&2; exit 1; }
+ cp "${installer}.sig" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe.sig"
+ chown "${HOST_UID}:${HOST_GID}" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe.sig"
'
}
@@ -558,6 +606,72 @@ build_linux_cli_archives() {
'
}
+sign_cli_archives() {
+ local version="$1"
+ local signing_key
+ local artifact
+
+ ensure_tauri_cli
+ signing_key="$(update_signing_key)"
+ for artifact in \
+ "downloads/iuna-v${version}-linux-x86_64.tar.gz" \
+ "downloads/iuna-v${version}-linux-aarch64.tar.gz"; do
+ [ -f "$artifact" ] || die "missing CLI update artifact: ${artifact}"
+ cargo tauri signer sign -f "$signing_key" -p "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD-}" "$artifact"
+ done
+}
+
+file_sha256() {
+ local file="$1"
+ if command -v sha256sum >/dev/null 2>&1; then
+ sha256sum "$file" | awk '{print $1}'
+ else
+ shasum -a 256 "$file" | awk '{print $1}'
+ fi
+}
+
+write_release_metadata() {
+ local version="$1"
+ local base="https://getiuna.org/downloads"
+ local linux_x86="iuna-v${version}-linux-x86_64.tar.gz"
+ local linux_arm="iuna-v${version}-linux-aarch64.tar.gz"
+ local mac="iuna-v${version}-macos-aarch64-desktop-update.app.tar.gz"
+ local windows="iuna-v${version}-windows-x86_64-desktop-setup.exe"
+
+ require_command jq
+ for file in "$linux_x86" "$linux_arm" "$mac" "$windows"; do
+ [ -f "downloads/$file" ] || die "missing release artifact: downloads/${file}"
+ [ -f "downloads/${file}.sig" ] || die "missing release signature: downloads/${file}.sig"
+ done
+
+ jq -n \
+ --arg tag "v${version}" \
+ --arg version "$version" \
+ --arg url "${base}/" \
+ --arg linux_x86_url "${base}/${linux_x86}" \
+ --arg linux_x86_sha "$(file_sha256 "downloads/$linux_x86")" \
+ --rawfile linux_x86_sig "downloads/${linux_x86}.sig" \
+ --arg linux_arm_url "${base}/${linux_arm}" \
+ --arg linux_arm_sha "$(file_sha256 "downloads/$linux_arm")" \
+ --rawfile linux_arm_sig "downloads/${linux_arm}.sig" \
+ '{tag: $tag, version: $version, url: $url, artifacts: {
+ "linux-x86_64": {url: $linux_x86_url, sha256: $linux_x86_sha, signature: $linux_x86_sig},
+ "linux-aarch64": {url: $linux_arm_url, sha256: $linux_arm_sha, signature: $linux_arm_sig}
+ }}' > downloads/latest.json
+
+ mkdir -p downloads/desktop
+ jq -n \
+ --arg version "$version" \
+ --arg mac_url "${base}/${mac}" \
+ --rawfile mac_sig "downloads/${mac}.sig" \
+ --arg windows_url "${base}/${windows}" \
+ --rawfile windows_sig "downloads/${windows}.sig" \
+ '{version: $version, platforms: {
+ "darwin-aarch64": {url: $mac_url, signature: $mac_sig},
+ "windows-x86_64": {url: $windows_url, signature: $windows_sig}
+ }}' > downloads/desktop/latest.json
+}
+
write_download_checksums() {
(
cd downloads
@@ -588,11 +702,14 @@ build_versions() {
local version="$1"
mkdir -p downloads
+ validate_update_public_key
build_linux_cli_archives "$version"
build_macos_desktop_if_possible "$version"
build_windows_desktop_if_possible "$version"
build_windows_desktop_in_docker_if_possible "$version"
require_desktop_artifacts "$version"
+ sign_cli_archives "$version"
+ write_release_metadata "$version"
write_download_checksums
}
@@ -841,7 +958,7 @@ main() {
exit 1
fi
run_release_tests "$skip_long_tests"
- build_linux_cli_archives "$version"
+ build_versions "$version"
build_docker_image "$version"
deploy_docker_image "$version" "$genesis"
exit 0
diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock
@@ -1224,9 +1224,9 @@ checksum = "e59fe57342dd136b22e8c7d1856e276b69e07a8f88153d218d2e54f19991a3eb"
[[package]]
name = "wasm-bindgen"
-version = "0.2.125"
+version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
+checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
dependencies = [
"cfg-if",
"once_cell",
@@ -1237,9 +1237,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.125"
+version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
+checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1247,22 +1247,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.125"
+version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
+checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.3",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.125"
+version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
+checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
dependencies = [
"unicode-ident",
]
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
@@ -10,7 +10,7 @@ publish = false
cargo-fuzz = true
[dependencies]
-iuna = { path = "..", features = ["fuzzing"] }
+iuna = { path = "..", default-features = false, features = ["fuzzing"] }
libfuzzer-sys = "0.4"
serde_json = "1"
diff --git a/nginx.conf b/nginx.conf
@@ -23,6 +23,12 @@ server {
try_files $uri =404;
}
+ location = /downloads/desktop/latest.json {
+ add_header Access-Control-Allow-Origin "*" always;
+ add_header Cache-Control "no-cache" always;
+ try_files $uri =404;
+ }
+
location /downloads/ {
autoindex on;
try_files $uri $uri/ =404;
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
@@ -48,6 +48,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
name = "atk"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -357,7 +366,7 @@ dependencies = [
"bitflags 2.13.1",
"core-foundation",
"core-graphics-types",
- "foreign-types",
+ "foreign-types 0.5.0",
"libc",
]
@@ -511,6 +520,17 @@ dependencies = [
]
[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
name = "derive_more"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -756,6 +776,16 @@ dependencies = [
]
[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -785,12 +815,21 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared 0.1.1",
+]
+
+[[package]]
+name = "foreign-types"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
- "foreign-types-shared",
+ "foreign-types-shared 0.3.1",
]
[[package]]
@@ -806,6 +845,12 @@ dependencies = [
[[package]]
name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "foreign-types-shared"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
@@ -1281,6 +1326,22 @@ dependencies = [
]
[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
name = "hyper-util"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1513,9 +1574,11 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
name = "iuna-desktop"
version = "0.4.29"
dependencies = [
+ "serde_json",
"tauri",
"tauri-build",
"tauri-plugin-shell",
+ "tauri-plugin-updater",
]
[[package]]
@@ -1688,6 +1751,12 @@ dependencies = [
]
[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
name = "litemap"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1741,6 +1810,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
+name = "minisign-verify"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
+
+[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1783,6 +1858,23 @@ dependencies = [
]
[[package]]
+name = "native-tls"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
name = "ndk"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1972,6 +2064,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.1",
"block2",
+ "libc",
"objc2",
"objc2-core-foundation",
]
@@ -1988,6 +2081,18 @@ dependencies = [
]
[[package]]
+name = "objc2-osa-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+]
+
+[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2062,6 +2167,49 @@ dependencies = [
]
[[package]]
+name = "openssl"
+version = "0.10.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
+dependencies = [
+ "bitflags 2.13.1",
+ "cfg-if",
+ "foreign-types 0.3.2",
+ "libc",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2078,6 +2226,20 @@ dependencies = [
]
[[package]]
+name = "osakit"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+ "objc2-osa-kit",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.19",
+]
+
+[[package]]
name = "pango"
version = "0.18.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2437,15 +2599,19 @@ dependencies = [
"http-body",
"http-body-util",
"hyper",
+ "hyper-tls",
"hyper-util",
"js-sys",
"log",
+ "native-tls",
"percent-encoding",
"pin-project-lite",
+ "rustls-pki-types",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
+ "tokio-native-tls",
"tokio-util",
"tower",
"tower-http",
@@ -2473,6 +2639,28 @@ dependencies = [
]
[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.13.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2488,6 +2676,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"
@@ -2545,6 +2742,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags 2.13.1",
+ "core-foundation",
+ "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 = "selectors"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3035,6 +3255,17 @@ dependencies = [
]
[[package]]
+name = "tar"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[package]]
name = "target-lexicon"
version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3191,6 +3422,38 @@ dependencies = [
]
[[package]]
+name = "tauri-plugin-updater"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b28d8cabdeb0564f03ae261963de4bc3d98321cd3d213e76a81b7d344e5df606"
+dependencies = [
+ "base64 0.22.1",
+ "dirs",
+ "flate2",
+ "futures-util",
+ "http",
+ "infer",
+ "log",
+ "minisign-verify",
+ "osakit",
+ "percent-encoding",
+ "reqwest",
+ "semver",
+ "serde",
+ "serde_json",
+ "tar",
+ "tauri",
+ "tauri-plugin",
+ "tempfile",
+ "thiserror 2.0.19",
+ "time",
+ "tokio",
+ "url",
+ "windows-sys 0.60.2",
+ "zip",
+]
+
+[[package]]
name = "tauri-runtime"
version = "2.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3291,6 +3554,19 @@ dependencies = [
]
[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "tendril"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3395,6 +3671,16 @@ dependencies = [
]
[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
name = "tokio-util"
version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3728,6 +4014,12 @@ dependencies = [
]
[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
name = "version-compare"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4500,6 +4792,16 @@ dependencies = [
]
[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
+[[package]]
name = "yoke"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4544,6 +4846,12 @@ dependencies = [
]
[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
name = "zerotrie"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4577,6 +4885,18 @@ dependencies = [
]
[[package]]
+name = "zip"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
+dependencies = [
+ "arbitrary",
+ "crc32fast",
+ "indexmap 2.14.0",
+ "memchr",
+]
+
+[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
@@ -10,5 +10,7 @@ publish = false
tauri-build = { version = "2", features = [] }
[dependencies]
+serde_json = "1"
tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-shell = "2"
+tauri-plugin-updater = { version = "2", default-features = false, features = ["native-tls", "zip"] }
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
@@ -4,8 +4,9 @@ use std::{
sync::{Mutex, OnceLock},
};
-use tauri::WindowEvent;
+use tauri::{AppHandle, State, WindowEvent};
use tauri_plugin_shell::{ShellExt, process::CommandChild};
+use tauri_plugin_updater::{Update, UpdaterExt};
#[cfg(any(target_os = "macos", target_os = "windows"))]
use tauri::{
@@ -16,13 +17,22 @@ use tauri::{
struct IunaSidecar(Mutex<Option<CommandChild>>);
struct IunaSleepInhibitor(Mutex<Option<SleepInhibitor>>);
+struct PendingDesktopUpdate(Mutex<Option<Update>>);
static SIDECAR: OnceLock<IunaSidecar> = OnceLock::new();
static SLEEP_INHIBITOR: OnceLock<IunaSleepInhibitor> = OnceLock::new();
fn main() {
+ let updater = tauri_plugin_updater::Builder::new()
+ .pubkey(include_str!("../../config/update-signing.key.pub").trim());
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
+ .plugin(updater.build())
+ .manage(PendingDesktopUpdate(Mutex::new(None)))
+ .invoke_handler(tauri::generate_handler![
+ check_desktop_update,
+ install_desktop_update
+ ])
.setup(|app| {
#[cfg(any(target_os = "macos", target_os = "windows"))]
setup_desktop_tray(app)?;
@@ -80,6 +90,47 @@ fn main() {
});
}
+#[tauri::command]
+async fn check_desktop_update(
+ app: AppHandle,
+ pending: State<'_, PendingDesktopUpdate>,
+) -> Result<Option<String>, String> {
+ let update = app
+ .updater()
+ .map_err(|error| error.to_string())?
+ .check()
+ .await
+ .map_err(|error| error.to_string())?;
+ let version = update.as_ref().map(|update| update.version.clone());
+ *pending.0.lock().map_err(|_| "updater mutex poisoned")? = update;
+ Ok(version)
+}
+
+#[tauri::command]
+async fn install_desktop_update(
+ app: AppHandle,
+ pending: State<'_, PendingDesktopUpdate>,
+) -> Result<(), String> {
+ let update = pending
+ .0
+ .lock()
+ .map_err(|_| "updater mutex poisoned")?
+ .take()
+ .ok_or_else(|| "no verified desktop update is ready".to_string())?;
+ let bytes = update
+ .download(|_, _| {}, || {})
+ .await
+ .map_err(|error| error.to_string())?;
+
+ stop_sidecar();
+ update.install(bytes).map_err(|error| error.to_string())?;
+
+ #[cfg(not(target_os = "windows"))]
+ app.restart();
+ #[cfg(target_os = "windows")]
+ Ok(())
+}
+
#[cfg(any(target_os = "macos", target_os = "windows"))]
fn setup_desktop_tray(app: &tauri::App) -> tauri::Result<()> {
const OPEN_MENU_ID: &str = "open-iuna";
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
@@ -26,6 +26,7 @@
},
"bundle": {
"active": true,
+ "createUpdaterArtifacts": true,
"targets": "all",
"externalBin": [
"binaries/iuna-sidecar"
@@ -46,7 +47,19 @@
}
},
"macOS": {
+ "signingIdentity": "-",
"minimumSystemVersion": "11.0"
}
+ },
+ "plugins": {
+ "updater": {
+ "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEEzMEJDQjQ4RDVDQUY1MTMKUldRVDljclZTTXNMbzgvU2tIenpMQjdWbnNhbE9NV1RjZGthQVNYdEhiUkcvdEl1TlZCZVBmYUwK",
+ "endpoints": [
+ "https://getiuna.org/downloads/desktop/latest.json"
+ ],
+ "windows": {
+ "installMode": "passive"
+ }
+ }
}
}
diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs
@@ -620,7 +620,7 @@ pub(super) const INDEX_HTML: &str = concat!(
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9.7 3.2 9.2 5.5a7.2 7.2 0 0 0-1.4.8L5.6 5.6 3.2 9.8l1.7 1.6a7.8 7.8 0 0 0 0 1.6l-1.7 1.6 2.4 4.2 2.2-.7a7.2 7.2 0 0 0 1.4.8l.5 2.3h4.8l.5-2.3a7.2 7.2 0 0 0 1.4-.8l2.2.7 2.4-4.2-1.7-1.6a7.8 7.8 0 0 0 0-1.6L21 9.8l-2.4-4.2-2.2.7a7.2 7.2 0 0 0-1.4-.8l-.5-2.3H9.7Z"></path><circle cx="12" cy="12.2" r="3.1"></circle></svg>
<span>Settings</span>
</button>
- <button class="version-panel" type="button" :class="{ update: updateAvailable(), checking: releaseCheckState === 'checking', failed: releaseCheckState === 'failed' }" :title="versionPanelTitle()" @click="openLatestRelease">
+ <button class="version-panel" type="button" :disabled="desktopUpdateBusy" :class="{ update: updateAvailable(), checking: releaseCheckState === 'checking', failed: releaseCheckState === 'failed' }" :title="versionPanelTitle()" @click="openLatestRelease">
<span class="version-dot" aria-hidden="true"></span>
<span class="version-label" x-text="appVersionLabel()"></span>
<span class="version-update" x-show="updateAvailable()">Update</span>
@@ -1578,6 +1578,25 @@ pub(super) const INDEX_HTML: &str = concat!(
</div>
</section>
</div>
+ <div class="setup-overlay transaction-overlay" x-show="desktopUpdateModalOpen" x-transition.opacity @click.self="closeDesktopUpdateModal()" role="dialog" aria-modal="true" aria-labelledby="desktop-update-title">
+ <section class="tx-modal">
+ <div class="tx-modal-head">
+ <div class="tx-modal-title">
+ <span class="pill transfer">Update</span>
+ <h2 id="desktop-update-title">Install <span x-text="latestReleaseLabel()"></span></h2>
+ </div>
+ <button type="button" @click="closeDesktopUpdateModal" :disabled="desktopUpdateBusy">Close</button>
+ </div>
+ <div class="info-copy">
+ <p>Iuna will download and verify the signed update, stop the local node, install it, and restart the desktop app.</p>
+ <p>Your wallet, settings, and local chain data stay on this device.</p>
+ </div>
+ <div class="danger-actions">
+ <button class="subtle" type="button" @click="closeDesktopUpdateModal" :disabled="desktopUpdateBusy">Later</button>
+ <button class="primary" type="button" @click="installDesktopUpdate" :disabled="desktopUpdateBusy" x-text="desktopUpdateBusy ? 'Installing...' : 'Install and restart'"></button>
+ </div>
+ </section>
+ </div>
<div class="setup-overlay transaction-overlay" x-show="chainResetModalOpen" x-transition.opacity @click.self="closeChainResetModal()" role="dialog" aria-modal="true" aria-labelledby="chain-reset-title">
<section class="tx-modal">
<div class="tx-modal-head">
diff --git a/src/cli.rs b/src/cli.rs
@@ -313,8 +313,11 @@ pub(crate) fn help_text() -> &'static str {
Usage:\n\
iuna [options]\n\
iuna --genesis [options]\n\
- iuna --join <addr:port> [options]\n\n\
+ iuna --join <addr:port> [options]\n\
+ iuna update [--check]\n\n\
Options:\n\
+ --version Print the installed iuna version\n\
+ update [--check] Install or only check for a signed CLI update\n\
--genesis Create a new chain with a fresh setup wallet\n\
--wallet <path> Wallet file (default <data-dir>/wallet.json)\n\
--chain-db <path> Chain SQLite database (default <data-dir>/chain.sqlite3)\n\
diff --git a/src/main.rs b/src/main.rs
@@ -29,6 +29,8 @@ use secrecy::{ExposeSecret, SecretString};
use tokio::sync::Mutex;
mod cli;
+#[cfg(feature = "cli-updater")]
+mod updater;
use cli::{
ChainMode, CliOptions, apply_cli_p2p_config_overrides, apply_cli_stratum_config_overrides,
apply_cli_wallet_endpoint_config_overrides, configured_p2p_announce_addr,
@@ -58,6 +60,10 @@ const WALLET_ENDPOINT_PORT_ENV: &str = "IUNA_WALLET_ENDPOINT_PORT";
#[tokio::main]
async fn main() -> Result<()> {
+ #[cfg(feature = "cli-updater")]
+ if updater::handle_cli_command().await? {
+ return Ok(());
+ }
let Some(opts) = CliOptions::parse()? else {
return Ok(());
};
diff --git a/src/main_tests.rs b/src/main_tests.rs
@@ -222,6 +222,17 @@ fn transaction_filters_default_to_every_transaction_type() {
}
#[test]
+fn management_ui_uses_the_native_desktop_updater_when_available() {
+ let html = include_str!("adapters/http/index_html.rs");
+ let javascript = include_str!("../www/assets/iuna-ui.js");
+
+ assert!(javascript.contains("invoke(\"check_desktop_update\")"));
+ assert!(javascript.contains("invoke(\"install_desktop_update\")"));
+ assert!(html.contains("Install and restart"));
+ assert!(!javascript.contains("window.confirm"));
+}
+
+#[test]
fn lightweight_wallet_recent_activity_does_not_use_view_all_filters() {
let javascript = include_str!("../wallet/app.js");
diff --git a/src/updater.rs b/src/updater.rs
@@ -0,0 +1,292 @@
+use std::{
+ collections::BTreeMap,
+ fs::{self, OpenOptions},
+ io::{Read, Write},
+ path::Path,
+};
+
+use anyhow::{Context, Result, bail};
+use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
+use flate2::read::GzDecoder;
+use minisign_verify::{PublicKey, Signature};
+use semver::Version;
+use serde::Deserialize;
+use sha2::{Digest, Sha256};
+
+const RELEASE_METADATA_URL: &str = "https://getiuna.org/downloads/latest.json";
+const UPDATE_PUBLIC_KEY: &str = include_str!("../config/update-signing.key.pub");
+const MAX_UPDATE_BYTES: u64 = 256 * 1024 * 1024;
+
+#[derive(Debug, Deserialize)]
+struct ReleaseMetadata {
+ version: String,
+ #[serde(default)]
+ artifacts: BTreeMap<String, ReleaseArtifact>,
+}
+
+#[derive(Debug, Deserialize)]
+struct ReleaseArtifact {
+ url: String,
+ sha256: String,
+ signature: String,
+}
+
+pub(crate) async fn handle_cli_command() -> Result<bool> {
+ let mut args = std::env::args().skip(1);
+ let Some(command) = args.next() else {
+ return Ok(false);
+ };
+
+ if matches!(command.as_str(), "--version" | "-V") {
+ if args.next().is_some() {
+ bail!("--version does not accept additional arguments");
+ }
+ println!("iuna {}", env!("CARGO_PKG_VERSION"));
+ return Ok(true);
+ }
+
+ if command != "update" {
+ return Ok(false);
+ }
+
+ let check_only = match args.next().as_deref() {
+ None => false,
+ Some("--check") => true,
+ Some(other) => bail!("unknown update option {other}; use `iuna update [--check]`"),
+ };
+ if let Some(other) = args.next() {
+ bail!("unexpected update argument {other}; use `iuna update [--check]`");
+ }
+
+ let release = fetch_release_metadata().await?;
+ let current = Version::parse(env!("CARGO_PKG_VERSION")).context("invalid built-in version")?;
+ let available = Version::parse(release.version.trim_start_matches('v'))
+ .context("release metadata contains an invalid version")?;
+
+ if available <= current {
+ println!("iuna v{current} is up to date");
+ return Ok(true);
+ }
+
+ println!("iuna v{available} is available (currently v{current})");
+ if check_only {
+ return Ok(true);
+ }
+
+ install_update(&release, &available).await?;
+ println!("updated iuna to v{available}; restart any running iuna service");
+ Ok(true)
+}
+
+async fn fetch_release_metadata() -> Result<ReleaseMetadata> {
+ let response = reqwest::Client::builder()
+ .timeout(std::time::Duration::from_secs(30))
+ .build()?
+ .get(RELEASE_METADATA_URL)
+ .header(reqwest::header::ACCEPT, "application/json")
+ .send()
+ .await
+ .context("could not fetch iuna release metadata")?
+ .error_for_status()
+ .context("iuna release metadata request failed")?;
+
+ response
+ .json()
+ .await
+ .context("could not decode iuna release metadata")
+}
+
+async fn install_update(release: &ReleaseMetadata, version: &Version) -> Result<()> {
+ let target = update_target()?;
+ let artifact = release
+ .artifacts
+ .get(target)
+ .with_context(|| format!("release v{version} has no artifact for {target}"))?;
+ let archive = download_artifact(artifact).await?;
+ verify_artifact(&archive, artifact)?;
+
+ let current_exe =
+ std::env::current_exe().context("could not locate the running iuna binary")?;
+ replace_executable(¤t_exe, &archive, version)
+}
+
+async fn download_artifact(artifact: &ReleaseArtifact) -> Result<Vec<u8>> {
+ let response = reqwest::Client::builder()
+ .timeout(std::time::Duration::from_secs(300))
+ .build()?
+ .get(&artifact.url)
+ .send()
+ .await
+ .context("could not download the iuna update")?
+ .error_for_status()
+ .context("iuna update download failed")?;
+
+ if response
+ .content_length()
+ .is_some_and(|size| size > MAX_UPDATE_BYTES)
+ {
+ bail!("iuna update is larger than the allowed 256 MiB");
+ }
+ let bytes = response
+ .bytes()
+ .await
+ .context("could not read the iuna update")?;
+ if bytes.len() as u64 > MAX_UPDATE_BYTES {
+ bail!("iuna update is larger than the allowed 256 MiB");
+ }
+ Ok(bytes.to_vec())
+}
+
+fn verify_artifact(bytes: &[u8], artifact: &ReleaseArtifact) -> Result<()> {
+ let actual_hash = format!("{:x}", Sha256::digest(bytes));
+ if !actual_hash.eq_ignore_ascii_case(&artifact.sha256) {
+ bail!("iuna update checksum verification failed");
+ }
+
+ let public_key_text = decode_tauri_signature(UPDATE_PUBLIC_KEY)
+ .context("the embedded iuna update public key is invalid")?;
+ let public_key = PublicKey::decode(&public_key_text)
+ .context("the embedded iuna update public key is invalid")?;
+ let signature_text = decode_tauri_signature(&artifact.signature)
+ .context("the iuna update signature is invalid")?;
+ let signature =
+ Signature::decode(&signature_text).context("the iuna update signature is invalid")?;
+ public_key
+ .verify(bytes, &signature, true)
+ .context("iuna update signature verification failed")
+}
+
+fn decode_tauri_signature(encoded: &str) -> Result<String> {
+ let decoded = BASE64
+ .decode(encoded.trim())
+ .context("invalid base64 in Tauri signature")?;
+ String::from_utf8(decoded).context("Tauri signature is not UTF-8")
+}
+
+fn replace_executable(current_exe: &Path, archive: &[u8], version: &Version) -> Result<()> {
+ let parent = current_exe
+ .parent()
+ .context("the running iuna binary has no parent directory")?;
+ let staged = parent.join(format!(".iuna-update-{version}-{}", std::process::id()));
+ let result = stage_binary(&staged, archive).and_then(|_| {
+ fs::rename(&staged, current_exe).with_context(|| {
+ format!(
+ "cannot replace {}; install iuna in a writable directory or run the update with sufficient permissions",
+ current_exe.display()
+ )
+ })
+ });
+ if result.is_err() {
+ let _ = fs::remove_file(&staged);
+ }
+ result
+}
+
+fn stage_binary(destination: &Path, archive: &[u8]) -> Result<()> {
+ let decoder = GzDecoder::new(archive);
+ let mut tar = tar::Archive::new(decoder);
+ let mut found = false;
+
+ for entry in tar
+ .entries()
+ .context("could not read the iuna update archive")?
+ {
+ let mut entry = entry.context("could not read an iuna update archive entry")?;
+ let path = entry
+ .path()
+ .context("invalid path in iuna update archive")?;
+ if path.file_name().and_then(|name| name.to_str()) != Some("iuna")
+ || !entry.header().entry_type().is_file()
+ {
+ continue;
+ }
+ if found {
+ bail!("iuna update archive contains multiple binaries");
+ }
+
+ let mut output = OpenOptions::new()
+ .create_new(true)
+ .write(true)
+ .open(destination)
+ .with_context(|| format!("cannot stage update at {}", destination.display()))?;
+ let mut buffer = [0_u8; 64 * 1024];
+ loop {
+ let count = entry.read(&mut buffer)?;
+ if count == 0 {
+ break;
+ }
+ output.write_all(&buffer[..count])?;
+ }
+ output.sync_all()?;
+ found = true;
+ }
+
+ if !found {
+ bail!("iuna update archive does not contain an iuna binary");
+ }
+
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ fs::set_permissions(destination, fs::Permissions::from_mode(0o755))?;
+ }
+ Ok(())
+}
+
+fn update_target() -> Result<&'static str> {
+ #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
+ return Ok("linux-x86_64");
+ #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
+ return Ok("linux-aarch64");
+ #[cfg(not(any(
+ all(target_os = "linux", target_arch = "x86_64"),
+ all(target_os = "linux", target_arch = "aarch64")
+ )))]
+ bail!("automatic CLI updates are not available for this platform")
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn release_metadata_remains_compatible_with_original_shape() {
+ let metadata: ReleaseMetadata = serde_json::from_str(
+ r#"{"tag":"v0.4.29","version":"0.4.29","url":"https://getiuna.org/downloads/"}"#,
+ )
+ .unwrap();
+ assert_eq!(metadata.version, "0.4.29");
+ assert!(metadata.artifacts.is_empty());
+ }
+
+ #[test]
+ fn archive_without_binary_is_rejected() {
+ let mut encoded = Vec::new();
+ {
+ let encoder =
+ flate2::write::GzEncoder::new(&mut encoded, flate2::Compression::default());
+ let mut archive = tar::Builder::new(encoder);
+ let mut header = tar::Header::new_gnu();
+ header.set_size(4);
+ header.set_cksum();
+ archive
+ .append_data(&mut header, "README.md", &b"test"[..])
+ .unwrap();
+ archive.into_inner().unwrap().finish().unwrap();
+ }
+ let temp = tempfile::tempdir().unwrap();
+ let error = stage_binary(&temp.path().join("iuna"), &encoded).unwrap_err();
+ assert!(error.to_string().contains("does not contain"));
+ }
+
+ #[test]
+ fn embedded_public_key_verifies_tauri_signature_format() {
+ const SIGNATURE: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVRVDljclZTTXNMbzZUQ3cwMUNibXF4bHhBZ0Vka0pCREhXS1VFWmNsbVc4ejVsUzZaN2ZNQ1VoVkwyV05nMGF2dzMrNjNMUlB5Wm1WS2JnbG4xVXhjV2s0N3dsMWx5aGdNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg5MDQwNjQ0CWZpbGU6dG1wLklaQThVTDRXOEEKRldRWG9UaXplbFAwVk5CUllNSHJDSSsyM2dLaXBPTXA0UWNOc0xtLyt2d2lLdTgvSmM3dWRZZmpqTkl2Q3RCMTlEWEx5dVNUK0gxeHlJQXY2VWhNQWc9PQo=";
+ let artifact = ReleaseArtifact {
+ url: "https://getiuna.org/test".to_string(),
+ sha256: format!("{:x}", Sha256::digest(b"iuna updater test vector")),
+ signature: SIGNATURE.to_string(),
+ };
+ verify_artifact(b"iuna updater test vector", &artifact).unwrap();
+ }
+}
diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js
@@ -47,6 +47,8 @@ window.iunaApp = function iunaApp() {
latestRelease: null,
releaseCheckState: "idle",
releaseCheckError: null,
+ desktopUpdateModalOpen: false,
+ desktopUpdateBusy: false,
config: { setup_complete: false },
auth: { configured: false, authenticated: false },
authLoaded: false,
@@ -303,6 +305,7 @@ window.iunaApp = function iunaApp() {
},
versionPanelTitle() {
+ if (this.desktopUpdateBusy) return "Installing update";
if (this.updateAvailable()) return `Update available: ${this.latestReleaseLabel()}`;
if (this.releaseCheckState === "failed") return this.releaseCheckError || "Could not check latest release";
if (this.releaseCheckState === "checking") return "Checking latest release";
@@ -311,6 +314,15 @@ window.iunaApp = function iunaApp() {
async openLatestRelease() {
const url = this.latestRelease?.url || IUNA_DOWNLOADS_URL;
+ const invoke = window.__TAURI__?.core?.invoke;
+ if (
+ this.updateAvailable()
+ && this.latestRelease?.desktopReady === true
+ && typeof invoke === "function"
+ ) {
+ this.desktopUpdateModalOpen = true;
+ return;
+ }
try {
const tauriOpen = window.__TAURI__?.shell?.open;
if (typeof tauriOpen === "function") {
@@ -321,6 +333,24 @@ window.iunaApp = function iunaApp() {
window.open(url, "_blank", "noopener,noreferrer");
},
+ closeDesktopUpdateModal() {
+ if (this.desktopUpdateBusy) return;
+ this.desktopUpdateModalOpen = false;
+ },
+
+ async installDesktopUpdate() {
+ const invoke = window.__TAURI__?.core?.invoke;
+ if (typeof invoke !== "function") return;
+ this.desktopUpdateBusy = true;
+ try {
+ await invoke("install_desktop_update");
+ } catch (error) {
+ this.desktopUpdateBusy = false;
+ this.desktopUpdateModalOpen = false;
+ this.showFlash(error?.message || String(error) || "Desktop update failed", "error");
+ }
+ },
+
showingSetup() {
return this.authLoaded && !this.showingAuth() && !this.showingNetworkMigration() && !this.config.setup_complete;
},
@@ -1177,9 +1207,17 @@ window.iunaApp = function iunaApp() {
if (!version) {
throw new Error("Release metadata is missing a version");
}
+ let desktopVersion = null;
+ const invoke = window.__TAURI__?.core?.invoke;
+ if (typeof invoke === "function") {
+ try {
+ desktopVersion = this.normalizeVersion(await invoke("check_desktop_update"));
+ } catch {}
+ }
this.latestRelease = {
tag: `v${version}`,
url: release.url || IUNA_DOWNLOADS_URL,
+ desktopReady: desktopVersion === version,
};
this.releaseCheckState = "done";
} catch (error) {
@@ -1368,6 +1406,7 @@ window.iunaApp = function iunaApp() {
},
closeModals() {
+ this.closeDesktopUpdateModal();
this.closeOptimizeWallet();
this.closeSendConfirmModal();
this.closeTransactionModal();