In-app updates on Windows
The Microsoft Store doesn't update EXE installs, so the app updates itself with Tauri's updater. It reads updater/latest.json from the Kestrove Gitea a minute after startup and once a day. A new version shows as a banner and in Settings > Updates, and installs only on "Update and restart". The vault locks before the installer takes over. The installer must be signed with the Kestrove updater key, with the signature bound to its version (requireSignedVersion), so a compromised download server can hold updates back but can't ship one or replay an old installer. Signing is a release step, after Authenticode: gitea-release.sh signs, uploads the .sig, and writes latest.json last. Plain builds need no key. Checks reveal only the IP address and app version, and can be turned off in Settings. endpoints is a list, so a mirror can be added. Linux packages update through their package manager. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -63,11 +63,17 @@ XWIN_ACCEPT_LICENSE=1 scripts/windows.sh
|
||||
`src-tauri/Cargo.toml`, tag the commit (`git tag v<version> && git push origin v<version>`), then:
|
||||
|
||||
```bash
|
||||
GITEA_TOKEN=<token with write:repository> scripts/gitea-release.sh
|
||||
GITEA_TOKEN=<token with write:repository> TAURI_SIGNING_PRIVATE_KEY_PATH=<updater key> scripts/gitea-release.sh
|
||||
```
|
||||
|
||||
It creates the release for the tag and uploads the installers, the browser helpers and a `SHA256SUMS` file.
|
||||
|
||||
**Offer it as an update (Windows):** the script also signs the installer with the updater key, uploads the
|
||||
signature, and writes `updater/latest.json`. Sign the installer with the code-signing certificate first, because
|
||||
the updater signature covers the final file. Installed copies are offered the update once that file is committed
|
||||
and pushed to main; the script prints the commands. Keep the updater key and its password offline and backed up:
|
||||
the app only accepts updates signed with it, so losing it means existing installs can't update.
|
||||
|
||||
## How the browser connection works
|
||||
|
||||
```
|
||||
@@ -84,6 +90,14 @@ browser extension ─(native messaging)─▶ kestrove-nmh ─(local connection)
|
||||
- The same code runs inside the Mac app; it lives in `kestrove-core` (`kv-ffi`'s browser bridge, `kv-ipc`,
|
||||
`kv-nmh`).
|
||||
|
||||
## Updates
|
||||
|
||||
On Windows, Kestrove checks for a new version a minute after it starts and once a day. The check is a plain request
|
||||
for `updater/latest.json` on the Kestrove Gitea: it reveals this computer's IP address and app version, and nothing
|
||||
from your vault. A new version appears as a banner and installs only when you choose **Update and restart**. The app
|
||||
verifies the download's signature before running it. Turn checks off in **Settings → Updates**. On Linux, updates
|
||||
come through your package manager.
|
||||
|
||||
## Where things are kept
|
||||
|
||||
The vault and the app's files are in your user's app-data folder (`vault.sqlite`, `pairings.json`,
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
#!/bin/sh
|
||||
# Creates (or reuses) the Gitea release for this version and uploads the desktop bundles to it.
|
||||
# Creates (or reuses) the Gitea release for this version, uploads the desktop bundles to it, and writes
|
||||
# updater/latest.json, which the Windows app checks for updates (src-tauri/src/updates.rs).
|
||||
#
|
||||
# GITEA_TOKEN=… scripts/gitea-release.sh [dir-with-files] (default ../release/desktop)
|
||||
# GITEA_TOKEN=… TAURI_SIGNING_PRIVATE_KEY_PATH=… scripts/gitea-release.sh [dir-with-files] (default ../release/desktop)
|
||||
#
|
||||
# GITEA_TOKEN: a Gitea access token with the write:repository scope (Settings → Applications). The tag
|
||||
# v<version> must already be pushed. Files: *.AppImage, *.deb, *.rpm, *-setup.exe, kestrove-nmh-*, SHA256SUMS.
|
||||
#
|
||||
# Updates: the Windows installer needs an updater signature (<installer>.sig) from the Kestrove updater key, whose
|
||||
# public half is in src-tauri/tauri.conf.json. Sign after Authenticode signing: the signature covers the final
|
||||
# file. With TAURI_SIGNING_PRIVATE_KEY_PATH set, a missing .sig is made here (the key's password is asked for, or
|
||||
# read from TAURI_SIGNING_PRIVATE_KEY_PASSWORD). Optional NOTES: the text shown with the update.
|
||||
# Users get the update only once updater/latest.json is committed and pushed to main (printed at the end).
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
: "${GITEA_TOKEN:?set GITEA_TOKEN (Gitea → Settings → Applications → token with write:repository)}"
|
||||
@@ -14,6 +21,15 @@ DIR=${1:-../release/desktop}
|
||||
V=$(node -p 'require("./package.json").version')
|
||||
TAG="v$V"
|
||||
AUTH="Authorization: token $GITEA_TOKEN"
|
||||
SETUP="$DIR/Kestrove_${V}_x64-setup.exe"
|
||||
|
||||
if [ -f "$SETUP" ] && [ ! -f "$SETUP.sig" ]; then
|
||||
: "${TAURI_SIGNING_PRIVATE_KEY_PATH:?$SETUP.sig is missing: set TAURI_SIGNING_PRIVATE_KEY_PATH to the updater key to sign it}"
|
||||
# --app-version binds the signature to this version (the app requires it), so an older signed installer can't
|
||||
# be offered again under a newer version number.
|
||||
npx tauri signer sign -f "$TAURI_SIGNING_PRIVATE_KEY_PATH" ${TAURI_SIGNING_PRIVATE_KEY_PASSWORD+-p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD"} \
|
||||
--app-version "$V" "$SETUP"
|
||||
fi
|
||||
|
||||
(cd "$DIR" && rm -f SHA256SUMS && shasum -a 256 -- *.AppImage *.deb *.rpm *-setup.exe kestrove-nmh-* 2>/dev/null > SHA256SUMS; cat SHA256SUMS)
|
||||
|
||||
@@ -22,9 +38,23 @@ if [ -z "$ID" ]; then
|
||||
BODY=$(node -e 'console.log(JSON.stringify({tag_name: process.argv[1], name: "Kestrove " + process.argv[2], body: "Kestrove desktop " + process.argv[2] + " for Windows and Linux. On a Mac, use the Kestrove app from the Mac App Store instead.\n\nVerify downloads with SHA256SUMS.", draft: false, prerelease: false}))' "$TAG" "$V")
|
||||
ID=$(curl -fsS -X POST -H "$AUTH" -H "Content-Type: application/json" -d "$BODY" "$API/repos/$REPO/releases" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).id))')
|
||||
fi
|
||||
for f in "$DIR"/*.AppImage "$DIR"/*.deb "$DIR"/*.rpm "$DIR"/*-setup.exe "$DIR"/kestrove-nmh-* "$DIR"/SHA256SUMS; do
|
||||
for f in "$DIR"/*.AppImage "$DIR"/*.deb "$DIR"/*.rpm "$DIR"/*-setup.exe "$DIR"/*-setup.exe.sig "$DIR"/kestrove-nmh-* "$DIR"/SHA256SUMS; do
|
||||
[ -f "$f" ] || continue
|
||||
echo "uploading $(basename "$f")"
|
||||
curl -fsS -X POST -H "$AUTH" -F "attachment=@$f" "$API/repos/$REPO/releases/$ID/assets?name=$(basename "$f")" >/dev/null
|
||||
done
|
||||
echo "Release: ${API%/api/v1}/$REPO/releases/tag/$TAG"
|
||||
|
||||
# The update manifest, written only after the installer it points to is uploaded.
|
||||
if [ -f "$SETUP.sig" ]; then
|
||||
mkdir -p updater
|
||||
node -e '
|
||||
const [v, url, sigFile, notes] = process.argv.slice(1);
|
||||
const signature = require("fs").readFileSync(sigFile, "utf8").trim();
|
||||
const doc = { version: v, notes, pub_date: new Date().toISOString().replace(/\.\d+Z$/, "Z"),
|
||||
platforms: { "windows-x86_64": { signature, url } } };
|
||||
require("fs").writeFileSync("updater/latest.json", JSON.stringify(doc, null, 2) + "\n");
|
||||
' "$V" "${API%/api/v1}/$REPO/releases/download/$TAG/$(basename "$SETUP")" "$SETUP.sig" "${NOTES:-Kestrove $V}"
|
||||
echo "Wrote updater/latest.json. To offer the update, commit it and push to main:"
|
||||
echo " git add updater/latest.json && git commit -m \"Offer $V as an update\" && git push origin main"
|
||||
fi
|
||||
|
||||
Generated
+284
-11
@@ -963,6 +963,16 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[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.1"
|
||||
@@ -986,7 +996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -999,7 +1009,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1698,6 +1708,16 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[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.14"
|
||||
@@ -2628,9 +2648,11 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2949,6 +2971,36 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[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.21",
|
||||
"walkdir",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[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.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.1"
|
||||
@@ -3017,7 +3069,7 @@ dependencies = [
|
||||
"arboard",
|
||||
"base64 0.22.1",
|
||||
"chacha20poly1305",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"getrandom 0.2.17",
|
||||
"hayro",
|
||||
"hkdf 0.12.4",
|
||||
@@ -3035,6 +3087,7 @@ dependencies = [
|
||||
"sha2 0.10.9",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-updater",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"url",
|
||||
@@ -3388,6 +3441,12 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "871285dc19d8d0ebe0eef3d0e99a205f2a71363b122632cbbfa4a6c370a960ce"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -3743,6 +3802,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
@@ -3758,6 +3818,18 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[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.2",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
@@ -3849,6 +3921,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e036b411754132fd6b74e94b74e9a13c5cf606fa93d1885c35860d3060820b1"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -3865,6 +3943,20 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[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.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
@@ -4513,15 +4605,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",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -4624,6 +4721,18 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.1"
|
||||
@@ -4633,6 +4742,33 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"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.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.16"
|
||||
@@ -4665,6 +4801,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
@@ -4782,7 +4927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
@@ -5063,6 +5208,22 @@ version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
|
||||
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 = "siphasher"
|
||||
version = "1.0.4"
|
||||
@@ -5292,6 +5453,27 @@ dependencies = [
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"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]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
@@ -5313,7 +5495,7 @@ checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"block2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
"dbus",
|
||||
@@ -5323,7 +5505,7 @@ dependencies = [
|
||||
"gdkwayland-sys",
|
||||
"gdkx11-sys",
|
||||
"gtk",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
@@ -5356,6 +5538,17 @@ dependencies = [
|
||||
"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 = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -5379,7 +5572,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"mime",
|
||||
@@ -5477,6 +5670,56 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67fb21a77e8f94e8beb3325e583102488659365af0a3948b7ea244c5b779c291"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"glob",
|
||||
"plist",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d957abbcb3f9f6c76d22a3a0f012f30880640d0bccb5634d2b12a9937e4c872b"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"dirs",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"libc",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest 0.13.5",
|
||||
"rustls",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tempfile",
|
||||
"thiserror 2.0.21",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.61.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.12.2"
|
||||
@@ -5487,7 +5730,7 @@ dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"objc2",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
@@ -5510,7 +5753,7 @@ checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
@@ -5584,7 +5827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -6461,6 +6704,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.10"
|
||||
@@ -6662,6 +6914,17 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
@@ -6927,7 +7190,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"ndk",
|
||||
"objc2",
|
||||
@@ -7003,6 +7266,16 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
||||
@@ -73,6 +73,8 @@ windows-sys = { version = "0.61", features = [
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
# In-app updates (updates.rs): signed installers listed in latest.json on the Kestrove Gitea.
|
||||
tauri-plugin-updater = "2"
|
||||
# Windows Hello (KeyCredentialManager); the same version Tauri already pulls in.
|
||||
windows = { version = "0.62", features = ["Foundation", "Security_Credentials", "Security_Cryptography", "Storage_Streams"] }
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod hello;
|
||||
pub mod qr;
|
||||
pub mod sync;
|
||||
pub mod title_bar;
|
||||
pub mod updates;
|
||||
|
||||
pub use kv_ffi::bridge::{broker, channel, passkey, words};
|
||||
pub use kv_ipc::{framing, ipc, private_file};
|
||||
|
||||
+59
-7
@@ -17,6 +17,7 @@ use kestrove_desktop::sync::{
|
||||
self, AddDeviceView, DeviceView, JoinConfirmView, JoinPollView, JoinView, SetupView, SignIn, SyncService,
|
||||
SyncStatus,
|
||||
};
|
||||
use kestrove_desktop::updates::{UpdateInfo, Updates};
|
||||
use kestrove_desktop::{ipc, private_file};
|
||||
use kv_ffi::{generate_password as core_generate_password, totp_code, CoreError, ItemDto, ItemKindDto, VaultCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -37,6 +38,7 @@ struct App {
|
||||
idle_minutes: Arc<AtomicU64>,
|
||||
/// Windows Hello is set up on this computer (checked off the main thread; see `refresh_hello`).
|
||||
hello_available: Arc<AtomicBool>,
|
||||
updates: Arc<Updates>,
|
||||
// Kept alive: on X11 the clipboard owner must outlive the copy.
|
||||
clipboard: Mutex<Option<arboard::Clipboard>>,
|
||||
}
|
||||
@@ -46,6 +48,15 @@ impl App {
|
||||
self.data_dir.join("settings.json")
|
||||
}
|
||||
|
||||
fn save_settings(&self) -> CmdResult<()> {
|
||||
let s = Settings {
|
||||
idle_minutes: self.idle_minutes.load(Ordering::Relaxed),
|
||||
check_updates: self.updates.automatic(),
|
||||
};
|
||||
let bytes = serde_json::to_vec(&s).map_err(|e| e.to_string())?;
|
||||
private_file::write(&self.settings_path(), &bytes).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Re-checks Windows Hello in the background (it can be set up or removed while Kestrove runs).
|
||||
fn refresh_hello(&self) {
|
||||
let available = self.hello_available.clone();
|
||||
@@ -58,6 +69,13 @@ impl App {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Settings {
|
||||
idle_minutes: u64,
|
||||
/// Check for a new version once a day (Windows; see `updates`).
|
||||
#[serde(default = "yes")]
|
||||
check_updates: bool,
|
||||
}
|
||||
|
||||
fn yes() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -280,8 +298,32 @@ fn touch(app: State<'_, App>) {
|
||||
fn set_idle_minutes(app: State<'_, App>, minutes: u64) -> CmdResult<()> {
|
||||
let minutes = minutes.clamp(1, 240);
|
||||
app.idle_minutes.store(minutes, Ordering::Relaxed);
|
||||
let bytes = serde_json::to_vec(&Settings { idle_minutes: minutes }).map_err(|e| e.to_string())?;
|
||||
private_file::write(&app.settings_path(), &bytes).map_err(|e| e.to_string())
|
||||
app.save_settings()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- updates (Windows; see `updates`)
|
||||
|
||||
#[tauri::command]
|
||||
fn update_info(app: State<'_, App>, handle: AppHandle) -> UpdateInfo {
|
||||
app.updates.info(&handle)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn check_update(app: State<'_, App>, handle: AppHandle) -> CmdResult<UpdateInfo> {
|
||||
Ok(app.updates.check(&handle).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_update_checks(app: State<'_, App>, enabled: bool) -> CmdResult<()> {
|
||||
app.updates.set_automatic(enabled);
|
||||
app.save_settings()
|
||||
}
|
||||
|
||||
/// "Update and restart": returns only if it failed; on success the installer replaces and restarts the app.
|
||||
#[tauri::command]
|
||||
async fn install_update(app: State<'_, App>, handle: AppHandle) -> CmdResult<()> {
|
||||
let core = app.core.clone();
|
||||
app.updates.install(&handle, move || core.lock()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -687,12 +729,14 @@ fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let ui = Arc::new(TauriUi { handle: app.handle().clone(), activity: activity.clone() });
|
||||
let broker = Broker::new(core.clone(), ui, PairingStore::open(data_dir.join("pairings.json"))?);
|
||||
|
||||
let idle_minutes = private_file::read(&data_dir.join("settings.json"))
|
||||
let settings = private_file::read(&data_dir.join("settings.json"))
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|b| serde_json::from_slice::<Settings>(&b).ok())
|
||||
.map_or(autolock::DEFAULT_IDLE.as_secs() / 60, |s| s.idle_minutes);
|
||||
.and_then(|b| serde_json::from_slice::<Settings>(&b).ok());
|
||||
let idle_minutes = settings.as_ref().map_or(autolock::DEFAULT_IDLE.as_secs() / 60, |s| s.idle_minutes);
|
||||
let idle_minutes = Arc::new(AtomicU64::new(idle_minutes));
|
||||
let updates = Updates::new(settings.as_ref().is_none_or(|s| s.check_updates));
|
||||
updates.clone().spawn(app.handle().clone());
|
||||
|
||||
let ipc_error = start_ipc(broker.clone()).err().map(|e| {
|
||||
eprintln!("kestrove: browser integration disabled: {e}");
|
||||
@@ -762,6 +806,7 @@ fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
ipc_error: Mutex::new(ipc_error),
|
||||
idle_minutes,
|
||||
hello_available: Arc::new(AtomicBool::new(false)),
|
||||
updates,
|
||||
clipboard: Mutex::new(None),
|
||||
});
|
||||
app.state::<App>().refresh_hello();
|
||||
@@ -769,7 +814,10 @@ fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
let builder = tauri::Builder::default();
|
||||
#[cfg(windows)]
|
||||
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
builder
|
||||
.setup(setup)
|
||||
.on_window_event(|window, event| {
|
||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
@@ -826,7 +874,11 @@ fn main() {
|
||||
cancel_add_device,
|
||||
approve_join,
|
||||
send_join,
|
||||
kit_qr
|
||||
kit_qr,
|
||||
update_info,
|
||||
check_update,
|
||||
set_update_checks,
|
||||
install_update
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running Kestrove");
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
//! In-app updates on Windows. The Microsoft Store doesn't update EXE installs, so the app does it itself.
|
||||
//!
|
||||
//! Tauri's updater fetches `latest.json` from the first address in tauri.conf.json → plugins.updater.endpoints
|
||||
//! that answers (the Kestrove Gitea; a mirror can be added after it), and offers the newer version. Nothing is
|
||||
//! downloaded until the user chooses "Update and restart". The installer must carry a signature from the
|
||||
//! Kestrove updater key, whose public half is in tauri.conf.json, so a compromised download server can hold
|
||||
//! updates back but can't ship one.
|
||||
//!
|
||||
//! The check is a plain HTTPS request for that file: it tells the server this computer's IP address and the
|
||||
//! app version, nothing from the vault. Settings can turn automatic checks off. Linux packages are updated by
|
||||
//! their package manager, so there `supported` is false and nothing is checked.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
/// After startup, so the check never competes with unlocking and the first sync.
|
||||
const FIRST_CHECK: Duration = Duration::from_secs(60);
|
||||
const EVERY: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct UpdateInfo {
|
||||
/// This build can update itself (Windows).
|
||||
pub supported: bool,
|
||||
pub current: String,
|
||||
/// A newer version, ready to install.
|
||||
pub available: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
/// Checks once a day on its own.
|
||||
pub automatic: bool,
|
||||
pub checked_at: Option<u64>,
|
||||
pub last_error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
#[cfg(windows)]
|
||||
pending: Option<tauri_plugin_updater::Update>,
|
||||
available: Option<(String, Option<String>)>,
|
||||
checked_at: Option<u64>,
|
||||
last_error: Option<String>,
|
||||
}
|
||||
|
||||
pub struct Updates {
|
||||
automatic: AtomicBool,
|
||||
state: Mutex<State>,
|
||||
}
|
||||
|
||||
impl Updates {
|
||||
pub fn new(automatic: bool) -> Arc<Self> {
|
||||
Arc::new(Self { automatic: AtomicBool::new(automatic), state: Mutex::default() })
|
||||
}
|
||||
|
||||
pub fn automatic(&self) -> bool {
|
||||
self.automatic.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn set_automatic(&self, on: bool) {
|
||||
self.automatic.store(on, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn info(&self, app: &AppHandle) -> UpdateInfo {
|
||||
let s = self.state.lock().unwrap_or_else(|e| e.into_inner());
|
||||
UpdateInfo {
|
||||
supported: cfg!(windows),
|
||||
current: app.package_info().version.to_string(),
|
||||
available: s.available.as_ref().map(|(v, _)| v.clone()),
|
||||
notes: s.available.as_ref().and_then(|(_, n)| n.clone()),
|
||||
automatic: self.automatic(),
|
||||
checked_at: s.checked_at,
|
||||
last_error: s.last_error.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks the update server now. Emits `update-available` when there is a newer version.
|
||||
pub async fn check(&self, app: &AppHandle) -> UpdateInfo {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
let result = match app.updater() {
|
||||
Ok(u) => u.check().await,
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
let found = {
|
||||
let mut s = self.state.lock().unwrap_or_else(|e| e.into_inner());
|
||||
s.checked_at = Some(now());
|
||||
match result {
|
||||
Ok(Some(update)) => {
|
||||
s.available = Some((update.version.clone(), update.body.clone()));
|
||||
s.pending = Some(update);
|
||||
s.last_error = None;
|
||||
true
|
||||
}
|
||||
Ok(None) => {
|
||||
(s.available, s.pending, s.last_error) = (None, None, None);
|
||||
false
|
||||
}
|
||||
// Offline or the server is down: keep what an earlier check found.
|
||||
Err(e) => {
|
||||
s.last_error = Some(format!("Couldn't check for updates: {e}"));
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
let info = self.info(app);
|
||||
if found {
|
||||
let _ = app.emit("update-available", &info);
|
||||
}
|
||||
info
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
self.info(app)
|
||||
}
|
||||
|
||||
/// Downloads the update the last check found, checks its signature and starts the installer. On success the
|
||||
/// app exits (after `before_exit`) and the installer starts the new version, so this returns only on failure.
|
||||
pub async fn install(&self, app: &AppHandle, before_exit: impl Fn() + Send + Sync + 'static) -> Result<(), String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
let pending = self.state.lock().unwrap_or_else(|e| e.into_inner()).pending.clone();
|
||||
let Some(update) = pending else { return Err("No update to install. Check again.".into()) };
|
||||
// The installer must be signed by the updater key; the builder reads the key and endpoints from
|
||||
// tauri.conf.json, plus the hook that locks the vault before the installer takes over.
|
||||
let updater = app.updater_builder().on_before_exit(before_exit).build().map_err(|e| e.to_string())?;
|
||||
let update = updater.check().await.map_err(|e| e.to_string())?.filter(|u| u.version == update.version);
|
||||
let update = update.ok_or("That update is no longer offered. Check again.")?;
|
||||
let (handle, mut done) = (app.clone(), 0u64);
|
||||
update
|
||||
.download_and_install(
|
||||
move |chunk, total| {
|
||||
done += chunk as u64;
|
||||
let _ = handle.emit("update-progress", (done, total));
|
||||
},
|
||||
|| {},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("The update couldn't be installed: {e}"))
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = (app, before_exit);
|
||||
Err("Install updates with your package manager.".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks a minute after startup, then once a day, while automatic checks are on.
|
||||
pub fn spawn(self: Arc<Self>, app: AppHandle) {
|
||||
if !cfg!(windows) {
|
||||
return;
|
||||
}
|
||||
tauri::async_runtime::spawn(async move {
|
||||
tokio::time::sleep(FIRST_CHECK).await;
|
||||
loop {
|
||||
if self.automatic() {
|
||||
self.check(&app).await;
|
||||
}
|
||||
tokio::time::sleep(EVERY).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn now() -> u64 {
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs())
|
||||
}
|
||||
@@ -47,5 +47,17 @@
|
||||
"installMode": "currentUser"
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDZFRUM4RDQ2Mzk0Rjg5Q0IKUldUTGlVODVSbzNzYml1ZTN3d0pBN1IwWkRoZXNROTRmbk5WOE5CR2VJQVYrTUJLN21PNzI4eHEK",
|
||||
"requireSignedVersion": true,
|
||||
"endpoints": [
|
||||
"https://gitea.samialabri.com/kestrove/kestrove-desktop/raw/branch/main/updater/latest.json"
|
||||
],
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+59
@@ -9,6 +9,7 @@ import {
|
||||
} from "./ui/kv";
|
||||
|
||||
interface HelloStatus { available: boolean; enabled: boolean; offered: boolean }
|
||||
interface UpdateInfo { supported: boolean; current: string; available: string | null; notes: string | null; automatic: boolean; checked_at: number | null; last_error: string | null }
|
||||
interface AppState { has_account: boolean; locked: boolean; remembers_secret_key: boolean; ipc_error: string | null; idle_minutes: number; hello: HelloStatus }
|
||||
interface ItemSummary { id: string; title: string; username: string | null; urls: string[]; favorite: boolean; has_totp: boolean; tags: string[] }
|
||||
interface SignInView { provider: string; provider_name: string; account: string | null }
|
||||
@@ -686,6 +687,7 @@ function showSync(s: SyncStatus, detail?: HTMLElement, state?: AppState) {
|
||||
if (s.restored_items > 0) {
|
||||
parts.push(banner("info", `${s.restored_items} item${s.restored_items > 1 ? "s" : ""} came back`, "They were deleted on another device but edited here."));
|
||||
}
|
||||
if (update?.available) parts.unshift(updateBanner(update.available));
|
||||
const now = iconButton("sync", "Sync now", async () => {
|
||||
now.disabled = true;
|
||||
await invoke("sync_now").catch(() => undefined);
|
||||
@@ -920,6 +922,7 @@ async function renderSettings(target: HTMLElement, state: AppState) {
|
||||
section("Devices", devicesSection),
|
||||
section("Emergency Kit", kitSection()),
|
||||
section("Paired browsers", list),
|
||||
updatesSection(await invoke<UpdateInfo>("update_info"), () => renderSettings(target, state)),
|
||||
));
|
||||
const status = await invoke<SyncStatus>("sync_status");
|
||||
renderSyncSettings(syncSection, status, () => renderSettings(target, state));
|
||||
@@ -1271,8 +1274,64 @@ function saveCard(s: SaveView): HTMLElement {
|
||||
h("div", "actions end", no, yes));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ updates (Windows; src-tauri/src/updates.rs)
|
||||
|
||||
let update: UpdateInfo | null = null;
|
||||
/// "Downloading… 40%" while an update downloads; the installer then closes the app.
|
||||
let updateStatus: HTMLElement | null = null;
|
||||
|
||||
function updateBanner(version: string): HTMLElement {
|
||||
updateStatus = h("span", "kv-caption kv-muted");
|
||||
const go = button("Update and restart", "text", async () => {
|
||||
go.disabled = true;
|
||||
updateStatus?.replaceChildren("Downloading…");
|
||||
try {
|
||||
await invoke("install_update");
|
||||
} catch (e) {
|
||||
updateStatus?.replaceChildren(String(e));
|
||||
go.disabled = false;
|
||||
}
|
||||
});
|
||||
return banner("info", `Kestrove ${version} is available`, updateStatus, go);
|
||||
}
|
||||
|
||||
async function refreshUpdate(info?: UpdateInfo) {
|
||||
update = info ?? (await invoke<UpdateInfo>("update_info").catch(() => null));
|
||||
if (syncBox?.isConnected) showSync(await invoke<SyncStatus>("sync_status"));
|
||||
}
|
||||
|
||||
function updatesSection(info: UpdateInfo, reload: () => Promise<void>): HTMLElement | null {
|
||||
if (!info.supported) return null;
|
||||
const auto = input({ type: "checkbox", checked: info.automatic });
|
||||
auto.className = "switch";
|
||||
auto.setAttribute("aria-label", "Check for updates automatically");
|
||||
auto.onchange = () => void invoke("set_update_checks", { enabled: auto.checked });
|
||||
const status = info.available
|
||||
? `Kestrove ${info.available} is available.`
|
||||
: info.last_error ?? (info.checked_at ? `Up to date. Checked ${ago(info.checked_at)}.` : "Not checked yet.");
|
||||
const check = button("Check now", "quiet", async () => {
|
||||
check.disabled = true;
|
||||
await refreshUpdate(await invoke<UpdateInfo>("check_update"));
|
||||
await reload();
|
||||
});
|
||||
const rows = h("div", "kv-card rows",
|
||||
h("div", "kv-frow", h("div", "val", h("span", "", `Kestrove ${info.current}`),
|
||||
h("span", `kv-hint ${info.last_error && !info.available ? "kv-error-text" : ""}`, status)), check),
|
||||
h("div", "kv-frow", h("div", "val", h("span", "", "Check for updates automatically"),
|
||||
h("span", "kv-hint", "Once a day, Kestrove asks its update server for the latest version. Nothing from your vault is sent. Updates install only when you choose.")), auto));
|
||||
if (info.available) rows.append(h("div", "kv-frow", updateBanner(info.available)));
|
||||
return section("Updates", rows);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ wiring
|
||||
|
||||
void refreshUpdate();
|
||||
void listen<UpdateInfo>("update-available", (e) => void refreshUpdate(e.payload));
|
||||
void listen<[number, number | null]>("update-progress", (e) => {
|
||||
const [done, total] = e.payload;
|
||||
updateStatus?.replaceChildren(total ? `Downloading… ${Math.floor((done / total) * 100)}%` : "Downloading…");
|
||||
});
|
||||
|
||||
void listen("pairing-request", () => render());
|
||||
void listen("save-request", () => render());
|
||||
void listen("passkey-request", () => render());
|
||||
|
||||
Reference in New Issue
Block a user