Integrity + defence-in-depth for the in-app updater:
- Require and verify a SHA-256 checksum before running an installer. Each
release must attach `<installer>.sha256`; downloadAppUpdate fetches it from
the host-pinned origin (deriving the URL itself, not trusting the renderer),
hashes the stream as it writes, and refuses to run on mismatch/missing/
unparseable. Fails closed. NB: same-host hash is integrity, not protection
against a fully compromised host — only Authenticode signing covers that.
- fetchTrustedText helper GETs the checksum with the same per-hop host
re-validation as the installer download, plus a size cap and timeout.
- finish() is now the single teardown point and aborts the request on failure,
so a write error can't leave Electron pulling bytes into a dead stream;
removed three now-redundant explicit abort() calls.
- Clear the idle/stall timer once the body is fully received.
- Export isTrustedDownloadUrl/compareVersions, extract extractSha256, and add
14 unit tests (host pin incl. userinfo spoof + wrong port, checksum parsing
incl. sha512 non-slice, prerelease never outranking its release).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review follow-ups on the in-app updater:
- Download via Electron net.request with redirect:'manual', re-validating the
host on EVERY hop. fetch() followed redirects automatically and undici hides
the Location header, so the trusted-host pin only covered the first URL — a
302 from the trusted host could bounce the download to an arbitrary server.
- Reject truncated downloads (received !== Content-Length) and an 'aborted'
mid-stream so a partial installer is never launched; clean up the temp file
on any failure.
- runAppUpdate: compare the parent dir by equality instead of startsWith(),
which a sibling like `<temp>_evil\x.exe` defeated.
- checkForAppUpdate: 15s AbortController timeout (mirrors the yt-dlp calls) and
pin htmlUrl to the trusted host before handing it to the OS browser.
- parseVersion: ignore -prerelease/+build suffixes so a prerelease never sorts
above its final release.
- SettingsView: funnel a failed check into the single error slot so two error
messages can't render at once.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new Settings → "Software update" card lets users update AeroFetch from inside
the app:
- src/main/updater.ts — queries the configured Gitea repo's latest release over
the public REST API, compares the tag to app.getVersion(), and (on request)
streams the installer asset to the OS temp dir with progress events, then
launches it and quits so it can replace the app.
- Security: only the trusted update host over HTTPS is ever downloaded from, and
only a freshly-downloaded .exe inside our temp dir is ever executed — the
download URL from the release JSON can't redirect us elsewhere. No token is
ever shipped; the source repo must be anonymously readable (public).
- IPC: app:update-check / -download / -run + an -progress push channel, exposed
via preload (checkForAppUpdate / downloadAppUpdate / runAppUpdate /
onAppUpdateProgress).
- UI: "Check for updates" → shows the new version and its release notes, then
"Update now" (with a download progress bar) or "View release". Up-to-date and
error states handled. Preview mock added so the flow is exercisable in `npm run ui`.
Note: the update source repo/host are constants at the top of updater.ts
(currently debont80/AeroFetch). Downloads work once the release repo is public
and the Gitea instance allows anonymous API + asset access.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>