fix(updater): make client auto-update work end-to-end and Linux server self-update verifiable

- client: update endpoint now sends {{target}}-{{arch}}-{{bundle_type}} so the
  server-echoed platforms key matches the updater plugin's
  {os}-{arch}-{installer} lookup (previously bare {{target}} produced a key
  the plugin never matches, so no update was ever surfaced)
- client: TOFU cert pin is scoped to the OwnCord server host via
  HostScopedVerifier; the GitHub installer download validates against web PKI
  instead of failing the pinned-fingerprint check on every install
- client: check/install share one build_updater helper so the two paths cannot
  diverge; tauri-plugin-updater minor-pinned per its configure_client guidance
- server: client-update endpoint serves target-specific artifacts (NSIS,
  per-arch AppImage) and returns 204 for targets without a published updater
  artifact (deb, darwin) instead of always serving the Windows NSIS installer
- release: server-update-manifest.json now binds both OS assets (legacy
  top-level pair kept pointing at the Windows binary so deployed servers still
  verify); VerifyReleaseManifest resolves the entry matching the downloaded
  asset, fixing Linux server self-update
- release: ARM64 staging renames installer, tar.gz and .sig consistently so
  signatures keep pairing and arch-less names cannot collide with x86_64 assets
- ci: run cargo test --lib (Rust #[cfg(test)] code was never compiled in CI);
  merge the two ptt tests that raced on the global PTT_VKEY atomic

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-07-23 18:38:22 +02:00
co-authored by Claude Fable 5
parent 3b1b6deb46
commit f3a89e0e09
13 changed files with 693 additions and 152 deletions
+8 -5
View File
@@ -58,11 +58,14 @@ func handleClientUpdate(u *updater.Updater) http.HandlerFunc {
return
}
// Find the .nsis.zip and .nsis.zip.sig assets from the release.
clientAssets := u.FindClientAssets()
nsisURL := clientAssets.InstallerURL
// Find the updater artifact and its signature for the requested
// target ("{os}-{arch}-{installer}", e.g. "windows-x86_64-nsis").
// Targets without a published updater artifact get 204 — never a
// foreign OS's or foreign installer's artifact.
clientAssets := u.FindClientAssets(target)
installerURL := clientAssets.InstallerURL
sigURL := clientAssets.SignatureURL
if nsisURL == "" || sigURL == "" {
if installerURL == "" || sigURL == "" {
w.WriteHeader(http.StatusNoContent)
return
}
@@ -82,7 +85,7 @@ func handleClientUpdate(u *updater.Updater) http.HandlerFunc {
Platforms: map[string]tauriPlatformResponse{
target: {
Signature: strings.TrimSpace(sigContent),
URL: nsisURL,
URL: installerURL,
},
},
}
+154 -18
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-chi/chi/v5"
@@ -12,7 +13,8 @@ import (
)
// fakeGitHubRelease returns a test HTTP server that mimics the GitHub
// Releases API, serving a release with the given tag and NSIS assets.
// Releases API, serving a release with the given tag and the client update
// assets for every platform the release workflow publishes.
// Asset download URLs point back to the test server so FetchTextAsset works.
func fakeGitHubRelease(t *testing.T, tag string) *httptest.Server {
t.Helper()
@@ -20,29 +22,40 @@ func fakeGitHubRelease(t *testing.T, tag string) *httptest.Server {
var srv *httptest.Server
mux := http.NewServeMux()
assetNames := []string{
"OwnCord_1.0.0_x64-setup.nsis.zip",
"OwnCord_1.0.0_x64-setup.nsis.zip.sig",
"OwnCord_1.0.0_amd64.AppImage.tar.gz",
"OwnCord_1.0.0_amd64.AppImage.tar.gz.sig",
"OwnCord_1.0.0_aarch64.AppImage.tar.gz",
"OwnCord_1.0.0_aarch64.AppImage.tar.gz.sig",
}
mux.HandleFunc("/repos/test/repo/releases/latest", func(w http.ResponseWriter, r *http.Request) {
assets := make([]map[string]any, 0, len(assetNames))
for _, name := range assetNames {
assets = append(assets, map[string]any{
"name": name,
"browser_download_url": srv.URL + "/download/" + name,
})
}
resp := map[string]any{
"tag_name": tag,
"body": "Release notes here",
"html_url": "https://github.com/test/repo/releases/" + tag,
"assets": []map[string]any{
{
"name": "OwnCord_1.0.0_x64-setup.nsis.zip",
"browser_download_url": srv.URL + "/download/OwnCord_1.0.0_x64-setup.nsis.zip",
},
{
"name": "OwnCord_1.0.0_x64-setup.nsis.zip.sig",
"browser_download_url": srv.URL + "/download/OwnCord_1.0.0_x64-setup.nsis.zip.sig",
},
},
"assets": assets,
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
})
// Serve the signature file content.
mux.HandleFunc("/download/OwnCord_1.0.0_x64-setup.nsis.zip.sig", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("dW50cnVzdGVkIGNvbW1lbnQ="))
// Serve signature file content for any .sig asset.
mux.HandleFunc("/download/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, ".sig") {
_, _ = w.Write([]byte("dW50cnVzdGVkIGNvbW1lbnQ="))
return
}
http.NotFound(w, r)
})
srv = httptest.NewServer(mux)
@@ -50,6 +63,25 @@ func fakeGitHubRelease(t *testing.T, tag string) *httptest.Server {
return srv
}
// platformEntry decodes the response body and returns the platforms entry for
// the given target, failing the test if it is missing.
func platformEntry(t *testing.T, rr *httptest.ResponseRecorder, target string) map[string]any {
t.Helper()
var resp map[string]any
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("decode: %v", err)
}
platforms, ok := resp["platforms"].(map[string]any)
if !ok {
t.Fatalf("response missing platforms map: %v", resp)
}
entry, ok := platforms[target].(map[string]any)
if !ok {
t.Fatalf("platforms missing key %q: %v", target, platforms)
}
return entry
}
func buildClientUpdateRouter(u *updater.Updater) http.Handler {
r := chi.NewRouter()
api.MountClientUpdateRoute(r, u)
@@ -63,7 +95,7 @@ func TestClientUpdate_NewVersionAvailable(t *testing.T) {
router := buildClientUpdateRouter(u)
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64/1.0.0", nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64-nsis/1.0.0", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
@@ -91,7 +123,7 @@ func TestClientUpdate_AlreadyLatest(t *testing.T) {
router := buildClientUpdateRouter(u)
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64/1.0.0", nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64-nsis/1.0.0", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
@@ -108,7 +140,111 @@ func TestClientUpdate_FutureVersion(t *testing.T) {
router := buildClientUpdateRouter(u)
// Client has a newer version than the release.
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64/2.0.0", nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64-nsis/2.0.0", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusNoContent {
t.Errorf("status = %d, want 204; body: %s", rr.Code, rr.Body.String())
}
}
func TestClientUpdate_WindowsTargetGetsNSISInstaller(t *testing.T) {
srv := fakeGitHubRelease(t, "v2.0.0")
u := updater.NewUpdater("1.0.0", "", "test", "repo")
u.SetBaseURL(srv.URL)
router := buildClientUpdateRouter(u)
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64-nsis/1.0.0", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body: %s", rr.Code, rr.Body.String())
}
entry := platformEntry(t, rr, "windows-x86_64-nsis")
url, _ := entry["url"].(string)
if !strings.HasSuffix(url, "_x64-setup.nsis.zip") {
t.Errorf("windows url = %q, want NSIS installer", url)
}
}
func TestClientUpdate_LinuxTargetGetsAppImage(t *testing.T) {
srv := fakeGitHubRelease(t, "v2.0.0")
u := updater.NewUpdater("1.0.0", "", "test", "repo")
u.SetBaseURL(srv.URL)
router := buildClientUpdateRouter(u)
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/linux-x86_64-appimage/1.0.0", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body: %s", rr.Code, rr.Body.String())
}
entry := platformEntry(t, rr, "linux-x86_64-appimage")
url, _ := entry["url"].(string)
if !strings.HasSuffix(url, "_amd64.AppImage.tar.gz") {
t.Errorf("linux url = %q, want x86_64 AppImage updater archive", url)
}
if sig, _ := entry["signature"].(string); sig == "" {
t.Error("linux platform entry missing signature")
}
}
func TestClientUpdate_LinuxArm64TargetGetsAarch64AppImage(t *testing.T) {
srv := fakeGitHubRelease(t, "v2.0.0")
u := updater.NewUpdater("1.0.0", "", "test", "repo")
u.SetBaseURL(srv.URL)
router := buildClientUpdateRouter(u)
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/linux-aarch64-appimage/1.0.0", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body: %s", rr.Code, rr.Body.String())
}
entry := platformEntry(t, rr, "linux-aarch64-appimage")
url, _ := entry["url"].(string)
if !strings.HasSuffix(url, "_aarch64.AppImage.tar.gz") {
t.Errorf("linux arm64 url = %q, want aarch64 AppImage updater archive", url)
}
}
func TestClientUpdate_UnsupportedTargetNoContent(t *testing.T) {
srv := fakeGitHubRelease(t, "v2.0.0")
u := updater.NewUpdater("1.0.0", "", "test", "repo")
u.SetBaseURL(srv.URL)
router := buildClientUpdateRouter(u)
// No darwin client is published; the updater must not be offered a
// Windows installer for it.
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/darwin-aarch64-app/1.0.0", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusNoContent {
t.Errorf("status = %d, want 204; body: %s", rr.Code, rr.Body.String())
}
}
func TestClientUpdate_DebTargetNoContent(t *testing.T) {
srv := fakeGitHubRelease(t, "v2.0.0")
u := updater.NewUpdater("1.0.0", "", "test", "repo")
u.SetBaseURL(srv.URL)
router := buildClientUpdateRouter(u)
// The release ships .deb packages but no signed deb UPDATER artifact.
// A deb client falling back to the AppImage archive would fail install
// forever (the plugin's install_deb rejects gzip bytes), so it must get
// 204 rather than an artifact for a different installer.
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/linux-x86_64-deb/1.0.0", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
@@ -129,7 +265,7 @@ func TestClientUpdate_GitHubError(t *testing.T) {
router := buildClientUpdateRouter(u)
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64/1.0.0", nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64-nsis/1.0.0", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
+27 -8
View File
@@ -14,30 +14,49 @@ import (
func TestFindClientAssets_NilCache(t *testing.T) {
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
ca := u.FindClientAssets()
ca := u.FindClientAssets("windows-x86_64-nsis")
if ca.InstallerURL != "" || ca.SignatureURL != "" {
t.Error("expected empty ClientAssets when no cache")
}
}
func TestFindClientAssets_WithMatchingAssets(t *testing.T) {
func TestFindClientAssets_ByTarget(t *testing.T) {
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
u.mu.Lock()
u.cache = &UpdateInfo{
Assets: []Asset{
{Name: "OwnCord_1.0.0_x64-setup.nsis.zip", DownloadURL: "https://example.com/installer.zip"},
{Name: "OwnCord_1.0.0_x64-setup.nsis.zip.sig", DownloadURL: "https://example.com/installer.zip.sig"},
{Name: "OwnCord_1.0.0_amd64.AppImage.tar.gz", DownloadURL: "https://example.com/amd64.AppImage.tar.gz"},
{Name: "OwnCord_1.0.0_amd64.AppImage.tar.gz.sig", DownloadURL: "https://example.com/amd64.AppImage.tar.gz.sig"},
{Name: "OwnCord_1.0.0_aarch64.AppImage.tar.gz", DownloadURL: "https://example.com/aarch64.AppImage.tar.gz"},
{Name: "OwnCord_1.0.0_aarch64.AppImage.tar.gz.sig", DownloadURL: "https://example.com/aarch64.AppImage.tar.gz.sig"},
{Name: "chatserver.exe", DownloadURL: "https://example.com/chatserver.exe"},
},
}
u.mu.Unlock()
ca := u.FindClientAssets()
if ca.InstallerURL != "https://example.com/installer.zip" {
t.Errorf("InstallerURL = %q, want installer URL", ca.InstallerURL)
cases := []struct {
target string
wantInstaller string
wantSig string
}{
{"windows-x86_64-nsis", "https://example.com/installer.zip", "https://example.com/installer.zip.sig"},
{"linux-x86_64-appimage", "https://example.com/amd64.AppImage.tar.gz", "https://example.com/amd64.AppImage.tar.gz.sig"},
{"linux-aarch64-appimage", "https://example.com/aarch64.AppImage.tar.gz", "https://example.com/aarch64.AppImage.tar.gz.sig"},
// No deb updater artifact is published — a deb client must get
// nothing rather than an AppImage archive its installer rejects.
{"linux-x86_64-deb", "", ""},
{"darwin-aarch64-app", "", ""},
{"windows-x86_64-unknown", "", ""},
{"", "", ""},
}
if ca.SignatureURL != "https://example.com/installer.zip.sig" {
t.Errorf("SignatureURL = %q, want signature URL", ca.SignatureURL)
for _, tc := range cases {
ca := u.FindClientAssets(tc.target)
if ca.InstallerURL != tc.wantInstaller || ca.SignatureURL != tc.wantSig {
t.Errorf("FindClientAssets(%q) = {%q, %q}, want {%q, %q}",
tc.target, ca.InstallerURL, ca.SignatureURL, tc.wantInstaller, tc.wantSig)
}
}
}
@@ -52,7 +71,7 @@ func TestFindClientAssets_NoMatchingAssets(t *testing.T) {
}
u.mu.Unlock()
ca := u.FindClientAssets()
ca := u.FindClientAssets("windows-x86_64-nsis")
if ca.InstallerURL != "" || ca.SignatureURL != "" {
t.Error("expected empty ClientAssets when no NSIS assets")
}
+88
View File
@@ -0,0 +1,88 @@
package updater
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"testing"
)
func testHash(seed string) string {
sum := sha256.Sum256([]byte(seed))
return hex.EncodeToString(sum[:])
}
// multiAssetManifest mirrors the exact JSON the release workflow generates:
// legacy top-level fields bind the Windows binary (kept so already-deployed
// servers, which only understand the single-asset schema, can still verify)
// and the assets list binds every OS.
func multiAssetManifest(exeHash, linuxHash string) []byte {
return fmt.Appendf(nil,
`{"version":"v1.0.0","asset":"chatserver.exe","sha256":"%s","assets":[{"asset":"chatserver.exe","sha256":"%s"},{"asset":"chatserver-linux-amd64.tar.gz","sha256":"%s"}]}`,
exeHash, exeHash, linuxHash)
}
func TestVerifyReleaseManifest_MultiAssetSelectsLinuxEntry(t *testing.T) {
u, key := newSignedTestUpdater(t, "", "1.0.0")
manifest := multiAssetManifest(testHash("exe"), testHash("linux"))
sig := signTestAsset(t, key, manifest)
got, err := u.VerifyReleaseManifest(manifest, sig, "v1.0.0", "chatserver-linux-amd64.tar.gz")
if err != nil {
t.Fatalf("VerifyReleaseManifest: %v", err)
}
if got.Asset != "chatserver-linux-amd64.tar.gz" || got.SHA256 != testHash("linux") {
t.Errorf("resolved binding = {%s, %s}, want linux entry", got.Asset, got.SHA256)
}
}
func TestVerifyReleaseManifest_MultiAssetSelectsWindowsEntry(t *testing.T) {
u, key := newSignedTestUpdater(t, "", "1.0.0")
manifest := multiAssetManifest(testHash("exe"), testHash("linux"))
sig := signTestAsset(t, key, manifest)
got, err := u.VerifyReleaseManifest(manifest, sig, "v1.0.0", "chatserver.exe")
if err != nil {
t.Fatalf("VerifyReleaseManifest: %v", err)
}
if got.Asset != "chatserver.exe" || got.SHA256 != testHash("exe") {
t.Errorf("resolved binding = {%s, %s}, want windows entry", got.Asset, got.SHA256)
}
}
func TestVerifyReleaseManifest_MultiAssetUnknownAssetFails(t *testing.T) {
u, key := newSignedTestUpdater(t, "", "1.0.0")
manifest := multiAssetManifest(testHash("exe"), testHash("linux"))
sig := signTestAsset(t, key, manifest)
if _, err := u.VerifyReleaseManifest(manifest, sig, "v1.0.0", "other.bin"); err == nil {
t.Error("expected error for asset the manifest does not bind")
}
}
func TestVerifyReleaseManifest_MultiAssetBadChecksumFails(t *testing.T) {
u, key := newSignedTestUpdater(t, "", "1.0.0")
manifest := fmt.Appendf(nil,
`{"version":"v1.0.0","asset":"chatserver.exe","sha256":"%s","assets":[{"asset":"chatserver-linux-amd64.tar.gz","sha256":"not-a-hash"}]}`,
testHash("exe"))
sig := signTestAsset(t, key, manifest)
if _, err := u.VerifyReleaseManifest(manifest, sig, "v1.0.0", "chatserver-linux-amd64.tar.gz"); err == nil {
t.Error("expected error for invalid checksum in assets entry")
}
}
func TestVerifyReleaseManifest_LegacySingleAssetStillVerifies(t *testing.T) {
u, key := newSignedTestUpdater(t, "", "1.0.0")
manifest := fmt.Appendf(nil,
`{"version":"v1.0.0","asset":"chatserver.exe","sha256":"%s"}`, testHash("exe"))
sig := signTestAsset(t, key, manifest)
got, err := u.VerifyReleaseManifest(manifest, sig, "v1.0.0", "chatserver.exe")
if err != nil {
t.Fatalf("VerifyReleaseManifest: %v", err)
}
if got.SHA256 != testHash("exe") {
t.Errorf("SHA256 = %s, want legacy hash", got.SHA256)
}
}
+61 -20
View File
@@ -76,8 +76,19 @@ type UpdateInfo struct {
type releaseManifest struct {
Version string `json:"version"`
Asset string `json:"asset"`
SHA256 string `json:"sha256"`
// Asset/SHA256 bind a single artifact. Releases before the multi-OS
// manifest bound only this pair; newer releases keep it pointing at the
// Windows binary so already-deployed servers can still verify and update.
Asset string `json:"asset"`
SHA256 string `json:"sha256"`
// Assets binds every server artifact the release ships (one per OS).
Assets []releaseManifestAsset `json:"assets,omitempty"`
}
// releaseManifestAsset is one artifact binding in a multi-OS release manifest.
type releaseManifestAsset struct {
Asset string `json:"asset"`
SHA256 string `json:"sha256"`
}
// Asset is a simplified release asset with name and download URL.
@@ -525,26 +536,37 @@ func (u *Updater) VerifyReleaseManifest(manifestData, signatureText []byte, expe
return releaseManifest{}, fmt.Errorf("parsing release manifest: %w", err)
}
manifest.Version = ensureVPrefix(strings.TrimSpace(manifest.Version))
manifest.Asset = strings.TrimSpace(manifest.Asset)
manifest.SHA256 = strings.ToLower(strings.TrimSpace(manifest.SHA256))
if manifest.Version == "" || manifest.Asset == "" || manifest.SHA256 == "" {
if manifest.Version == "v" {
return releaseManifest{}, fmt.Errorf("release manifest is missing required fields")
}
if manifest.Version != ensureVPrefix(expectedVersion) {
return releaseManifest{}, fmt.Errorf("release manifest version %q does not match release %q", manifest.Version, ensureVPrefix(expectedVersion))
}
if manifest.Asset != expectedAsset {
return releaseManifest{}, fmt.Errorf("release manifest asset %q does not match expected asset %q", manifest.Asset, expectedAsset)
}
if len(manifest.SHA256) != sha256.Size*2 {
return releaseManifest{}, fmt.Errorf("release manifest checksum for %s has invalid length", manifest.Asset)
}
if _, err := hex.DecodeString(manifest.SHA256); err != nil {
return releaseManifest{}, fmt.Errorf("release manifest checksum for %s is invalid: %w", manifest.Asset, err)
}
return manifest, nil
// Candidate bindings: the per-OS assets list plus the legacy single-asset
// pair (the only binding manifests from older releases carry).
candidates := append([]releaseManifestAsset{}, manifest.Assets...)
candidates = append(candidates, releaseManifestAsset{Asset: manifest.Asset, SHA256: manifest.SHA256})
for _, c := range candidates {
asset := strings.TrimSpace(c.Asset)
if asset == "" || asset != expectedAsset {
continue
}
sum := strings.ToLower(strings.TrimSpace(c.SHA256))
if len(sum) != sha256.Size*2 {
return releaseManifest{}, fmt.Errorf("release manifest checksum for %s has invalid length", asset)
}
if _, err := hex.DecodeString(sum); err != nil {
return releaseManifest{}, fmt.Errorf("release manifest checksum for %s is invalid: %w", asset, err)
}
// Normalize the returned binding to the matched entry so callers can
// keep reading manifest.Asset/manifest.SHA256 regardless of schema.
manifest.Asset = asset
manifest.SHA256 = sum
return manifest, nil
}
return releaseManifest{}, fmt.Errorf("release manifest does not bind expected asset %q", expectedAsset)
}
// VerifySignature checks whether the detached minisign signature matches the
@@ -720,9 +742,28 @@ func (u *Updater) fetchBody(ctx context.Context, url string) ([]byte, error) {
return io.ReadAll(io.LimitReader(resp.Body, maxFetchBytes))
}
// FindClientAssets scans the cached release assets for the Tauri NSIS
// installer zip and its Ed25519 signature file.
func (u *Updater) FindClientAssets() ClientAssets {
// clientAssetSuffixByTarget maps a Tauri updater target
// ("{os}-{arch}-{installer}") to the release asset suffix for that platform's
// updater artifact. The matching signature asset is the same suffix plus
// ".sig". Targets without a published updater artifact are absent — notably
// linux-*-deb: the release ships .deb packages but no signed deb updater
// artifact, and serving the AppImage archive instead would make the plugin's
// install_deb reject every update.
var clientAssetSuffixByTarget = map[string]string{
"windows-x86_64-nsis": "_x64-setup.nsis.zip",
"linux-x86_64-appimage": "_amd64.AppImage.tar.gz",
"linux-aarch64-appimage": "_aarch64.AppImage.tar.gz",
}
// FindClientAssets scans the cached release assets for the client updater
// artifact and its signature matching the given Tauri updater target
// (e.g. "windows-x86_64-nsis"). Unknown targets return empty ClientAssets.
func (u *Updater) FindClientAssets(target string) ClientAssets {
suffix, ok := clientAssetSuffixByTarget[target]
if !ok {
return ClientAssets{}
}
u.mu.Lock()
defer u.mu.Unlock()
@@ -733,9 +774,9 @@ func (u *Updater) FindClientAssets() ClientAssets {
var ca ClientAssets
for _, a := range u.cache.Assets {
switch {
case strings.HasSuffix(a.Name, "_x64-setup.nsis.zip.sig"):
case strings.HasSuffix(a.Name, suffix+".sig"):
ca.SignatureURL = a.DownloadURL
case strings.HasSuffix(a.Name, "_x64-setup.nsis.zip"):
case strings.HasSuffix(a.Name, suffix):
ca.InstallerURL = a.DownloadURL
}
}