feat: sha256 checksum for self-updates

This commit is contained in:
zhom
2026-07-11 15:41:00 +04:00
parent 86d58717b4
commit eeb5c816bf
15 changed files with 418 additions and 14 deletions
+13 -1
View File
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { AppUpdateToast } from "@/components/app-update-toast";
import { translateBackendError } from "@/lib/backend-errors";
import { showToast } from "@/lib/toast-utils";
import type { AppUpdateInfo, AppUpdateProgress } from "@/types";
@@ -82,11 +83,16 @@ export function useAppUpdateNotifications() {
showToast({
type: "error",
title: t("appUpdate.toast.updateFailed"),
description: String(error),
description: translateBackendError(t, error),
duration: 6000,
});
setIsUpdating(false);
setUpdateProgress(null);
// Deliberately NOT resetting autoDownloadedVersion here: the
// auto-download effect re-runs as soon as isUpdating flips back to
// false, so clearing the marker now would retry in a tight loop.
// Retries are re-armed when the next backend check delivers a fresh
// update event instead.
}
},
[t],
@@ -127,6 +133,12 @@ export function useAppUpdateNotifications() {
"app-update-available",
(event) => {
console.log("App update available:", event.payload);
// A fresh backend check re-arms auto-download, so a version whose
// earlier attempt failed (e.g. a transient checksum-fetch error) is
// retried once per periodic check instead of staying blocked until
// restart. The effect's updateReady guard keeps an already-prepared
// update from being downloaded again.
autoDownloadedVersion.current = null;
setUpdateInfo(event.payload);
},
);
+3 -1
View File
@@ -1798,7 +1798,9 @@
"proxyNotWorking": "The selected proxy isn't working, so the profile wasn't created.",
"proxyPaymentRequired": "The selected proxy requires payment (402) — its subscription may have expired — so the profile wasn't created.",
"vpnNotWorking": "The selected VPN isn't working, so the profile wasn't created.",
"camoufoxImportDeprecated": "Importing Firefox-based profiles is no longer supported. Please use Wayfern instead."
"camoufoxImportDeprecated": "Importing Firefox-based profiles is no longer supported. Please use Wayfern instead.",
"updateChecksumsUnavailable": "The update {{version}} could not be verified because its checksum file could not be retrieved. The update was not installed; it will be retried later.",
"updateChecksumMismatch": "The downloaded update file {{file}} failed checksum verification and was discarded. Please try again."
},
"rail": {
"profiles": "Profiles",
+3 -1
View File
@@ -1798,7 +1798,9 @@
"proxyNotWorking": "El proxy seleccionado no funciona, por lo que no se creó el perfil.",
"proxyPaymentRequired": "El proxy seleccionado requiere pago (402) —su suscripción puede haber vencido— por lo que no se creó el perfil.",
"vpnNotWorking": "La VPN seleccionada no funciona, por lo que no se creó el perfil.",
"camoufoxImportDeprecated": "La importación de perfiles basados en Firefox ya no está soportada. Utilice Wayfern en su lugar."
"camoufoxImportDeprecated": "La importación de perfiles basados en Firefox ya no está soportada. Utilice Wayfern en su lugar.",
"updateChecksumsUnavailable": "No se pudo verificar la actualización {{version}} porque no se pudo obtener su archivo de sumas de comprobación. La actualización no se instaló; se reintentará más tarde.",
"updateChecksumMismatch": "El archivo de actualización descargado {{file}} no superó la verificación de suma de comprobación y fue descartado. Inténtalo de nuevo."
},
"rail": {
"profiles": "Perfiles",
+3 -1
View File
@@ -1798,7 +1798,9 @@
"proxyNotWorking": "Le proxy sélectionné ne fonctionne pas, le profil n'a donc pas été créé.",
"proxyPaymentRequired": "Le proxy sélectionné requiert un paiement (402) — son abonnement a peut-être expiré — le profil n'a donc pas été créé.",
"vpnNotWorking": "Le VPN sélectionné ne fonctionne pas, le profil n'a donc pas été créé.",
"camoufoxImportDeprecated": "L'importation de profils basés sur Firefox n'est plus prise en charge. Utilisez Wayfern à la place."
"camoufoxImportDeprecated": "L'importation de profils basés sur Firefox n'est plus prise en charge. Utilisez Wayfern à la place.",
"updateChecksumsUnavailable": "La mise à jour {{version}} n'a pas pu être vérifiée car son fichier de sommes de contrôle n'a pas pu être récupéré. La mise à jour n'a pas été installée ; une nouvelle tentative aura lieu plus tard.",
"updateChecksumMismatch": "Le fichier de mise à jour téléchargé {{file}} a échoué à la vérification de la somme de contrôle et a été supprimé. Veuillez réessayer."
},
"rail": {
"profiles": "Profils",
+3 -1
View File
@@ -1798,7 +1798,9 @@
"proxyNotWorking": "選択したプロキシが機能していないため、プロファイルは作成されませんでした。",
"proxyPaymentRequired": "選択したプロキシは支払いが必要です(402)。サブスクリプションが期限切れの可能性があります。そのため、プロファイルは作成されませんでした。",
"vpnNotWorking": "選択したVPNが機能していないため、プロファイルは作成されませんでした。",
"camoufoxImportDeprecated": "Firefox ベースのプロファイルのインポートはサポートされなくなりました。Wayfern をご利用ください。"
"camoufoxImportDeprecated": "Firefox ベースのプロファイルのインポートはサポートされなくなりました。Wayfern をご利用ください。",
"updateChecksumsUnavailable": "アップデート {{version}} のチェックサムファイルを取得できなかったため、検証できませんでした。アップデートはインストールされませんでした。後で再試行されます。",
"updateChecksumMismatch": "ダウンロードしたアップデートファイル {{file}} はチェックサム検証に失敗したため破棄されました。もう一度お試しください。"
},
"rail": {
"profiles": "プロファイル",
+3 -1
View File
@@ -1798,7 +1798,9 @@
"proxyNotWorking": "선택한 프록시가 작동하지 않아 프로필이 생성되지 않았습니다.",
"proxyPaymentRequired": "선택한 프록시는 결제가 필요합니다(402). 구독이 만료되었을 수 있어 프로필이 생성되지 않았습니다.",
"vpnNotWorking": "선택한 VPN이 작동하지 않아 프로필이 생성되지 않았습니다.",
"camoufoxImportDeprecated": "Firefox 기반 프로필 가져오기는 더 이상 지원되지 않습니다. Wayfern을 사용하세요."
"camoufoxImportDeprecated": "Firefox 기반 프로필 가져오기는 더 이상 지원되지 않습니다. Wayfern을 사용하세요.",
"updateChecksumsUnavailable": "업데이트 {{version}}의 체크섬 파일을 가져올 수 없어 검증하지 못했습니다. 업데이트가 설치되지 않았으며 나중에 다시 시도됩니다.",
"updateChecksumMismatch": "다운로드한 업데이트 파일 {{file}}이(가) 체크섬 검증에 실패하여 삭제되었습니다. 다시 시도해 주세요."
},
"rail": {
"profiles": "프로필",
+3 -1
View File
@@ -1798,7 +1798,9 @@
"proxyNotWorking": "O proxy selecionado não está funcionando, então o perfil não foi criado.",
"proxyPaymentRequired": "O proxy selecionado exige pagamento (402) — sua assinatura pode ter expirado — então o perfil não foi criado.",
"vpnNotWorking": "A VPN selecionada não está funcionando, então o perfil não foi criado.",
"camoufoxImportDeprecated": "A importação de perfis baseados em Firefox não é mais suportada. Use o Wayfern."
"camoufoxImportDeprecated": "A importação de perfis baseados em Firefox não é mais suportada. Use o Wayfern.",
"updateChecksumsUnavailable": "Não foi possível verificar a atualização {{version}} porque o arquivo de somas de verificação não pôde ser obtido. A atualização não foi instalada; será tentada novamente mais tarde.",
"updateChecksumMismatch": "O arquivo de atualização baixado {{file}} falhou na verificação de soma de verificação e foi descartado. Tente novamente."
},
"rail": {
"profiles": "Perfis",
+3 -1
View File
@@ -1798,7 +1798,9 @@
"proxyNotWorking": "Выбранный прокси не работает, поэтому профиль не создан.",
"proxyPaymentRequired": "Выбранный прокси требует оплаты (402) — возможно, его подписка истекла — поэтому профиль не создан.",
"vpnNotWorking": "Выбранный VPN не работает, поэтому профиль не создан.",
"camoufoxImportDeprecated": "Импорт профилей на основе Firefox больше не поддерживается. Используйте Wayfern."
"camoufoxImportDeprecated": "Импорт профилей на основе Firefox больше не поддерживается. Используйте Wayfern.",
"updateChecksumsUnavailable": "Не удалось проверить обновление {{version}}: файл контрольных сумм не удалось получить. Обновление не было установлено; попытка будет повторена позже.",
"updateChecksumMismatch": "Загруженный файл обновления {{file}} не прошёл проверку контрольной суммы и был удалён. Попробуйте ещё раз."
},
"rail": {
"profiles": "Профили",
+3 -1
View File
@@ -1798,7 +1798,9 @@
"proxyNotWorking": "Proxy đã chọn không hoạt động, nên profile chưa được tạo.",
"proxyPaymentRequired": "Proxy đã chọn yêu cầu thanh toán (402) — gói đăng ký của nó có thể đã hết hạn — nên profile chưa được tạo.",
"vpnNotWorking": "VPN đã chọn không hoạt động, nên profile chưa được tạo.",
"camoufoxImportDeprecated": "Không còn hỗ trợ nhập profile dựa trên Firefox. Vui lòng dùng Wayfern."
"camoufoxImportDeprecated": "Không còn hỗ trợ nhập profile dựa trên Firefox. Vui lòng dùng Wayfern.",
"updateChecksumsUnavailable": "Không thể xác minh bản cập nhật {{version}} vì không thể tải tệp checksum. Bản cập nhật chưa được cài đặt; sẽ thử lại sau.",
"updateChecksumMismatch": "Tệp cập nhật đã tải xuống {{file}} không vượt qua kiểm tra checksum và đã bị loại bỏ. Vui lòng thử lại."
},
"rail": {
"profiles": "Profile",
+3 -1
View File
@@ -1798,7 +1798,9 @@
"proxyNotWorking": "所选代理无法使用,因此未创建配置文件。",
"proxyPaymentRequired": "所选代理需要付费(402),其订阅可能已过期,因此未创建配置文件。",
"vpnNotWorking": "所选 VPN 无法使用,因此未创建配置文件。",
"camoufoxImportDeprecated": "不再支持导入基于 Firefox 的配置文件。请改用 Wayfern。"
"camoufoxImportDeprecated": "不再支持导入基于 Firefox 的配置文件。请改用 Wayfern。",
"updateChecksumsUnavailable": "无法验证更新 {{version}}:无法获取其校验和文件。更新未安装,稍后将重试。",
"updateChecksumMismatch": "下载的更新文件 {{file}} 未通过校验和验证,已被丢弃。请重试。"
},
"rail": {
"profiles": "配置文件",
+10
View File
@@ -34,6 +34,8 @@ export type BackendErrorCode =
| "PROXY_PAYMENT_REQUIRED"
| "VPN_NOT_WORKING"
| "CAMOUFOX_IMPORT_DEPRECATED"
| "UPDATE_CHECKSUMS_UNAVAILABLE"
| "UPDATE_CHECKSUM_MISMATCH"
| "INTERNAL_ERROR";
export interface BackendError {
@@ -138,6 +140,14 @@ export function translateBackendError(t: TFunction, err: unknown): string {
return t("backendErrors.vpnNotWorking");
case "CAMOUFOX_IMPORT_DEPRECATED":
return t("backendErrors.camoufoxImportDeprecated");
case "UPDATE_CHECKSUMS_UNAVAILABLE":
return t("backendErrors.updateChecksumsUnavailable", {
version: parsed.params?.version ?? "",
});
case "UPDATE_CHECKSUM_MISMATCH":
return t("backendErrors.updateChecksumMismatch", {
file: parsed.params?.file ?? "",
});
case "INTERNAL_ERROR":
return t("backendErrors.internal", {
detail: parsed.params?.detail ?? "",
+4
View File
@@ -211,6 +211,10 @@ export interface AppUpdateInfo {
manual_update_required: boolean;
release_page_url?: string;
repo_update: boolean;
/** URL of the release's SHA256SUMS.txt; downloads are verified against it. */
checksums_url?: string | null;
/** GitHub-computed digest of the chosen asset ("sha256:<hex>"). */
asset_digest?: string | null;
}
export interface AppUpdateProgress {