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:
+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