feat(setup): fetch ffmpeg on first run instead of bundling it

Ship a smaller installer that no longer carries ffmpeg.exe/ffprobe.exe
(the bulk of its size). On first run they are downloaded from a pinned
upstream archive (gyan 8.1.2), verified against a pinned SHA-256, and
unpacked with the OS tar.exe into the managed userData/bin dir -- the
same place as the self-updating yt-dlp, so they survive app updates and
portable re-extraction. A hard onboarding gate blocks the app until they
are present, with a "locate existing ffmpeg" folder-picker fallback for
offline machines and a Repair action in Settings > About.

The updater's streaming/checksum/redirect/idle-timeout download loop is
extracted to lib/verifiedDownload.streamVerifiedFile and shared by both
the app-installer download and the ffmpeg fetch; the updater's public
behaviour and error strings are unchanged (its boundary tests still pass).
No new npm dependency: extraction uses the System32 bsdtar resolved by
absolute path (audit F3).

Note: this commit also carries pre-existing, in-progress aria2c/network
and updater-token work that was already uncommitted in the working tree
and is entangled with the above in shared files (updater.ts, ipc.ts,
preload/index.ts, mockApi.ts, shared/ipc.ts, plus the settings/network
files and aria2c.exe). It was not cleanly separable by path, so it is
included here rather than split out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 11:02:13 -04:00
parent cb25262a2d
commit eb53de2ea5
33 changed files with 1391 additions and 386 deletions
+35 -25
View File
@@ -4,33 +4,43 @@
* timeouts, caps, tickers) and from user settings (settings.ts). Retarget a
* fork's update source by editing these three values.
*
* The update repo is PRIVATE, so the release API + downloads require a token.
* A build-time read-only token (BAKED_UPDATE_TOKEN below) lets a shipped client
* reach it without the user configuring anything; a user-set updateToken in
* Settings still takes precedence. On a build with no token baked in, the update
* check simply reports that it couldn't reach the server.
* The update source is a PUBLIC, releases-only repo — it holds no source, just
* the published installers — so anonymous reads work and the updater sends no
* auth at all. Keeping releases separate from the (private) source repo is what
* lets the client stay tokenless: there is no secret in the bundle to leak. A
* private fork retargets its updater by editing these constants and rebuilding.
*/
export const UPDATE_HOST = 'https://gitea.netbird.zimspace.uk:5938'
export const UPDATE_OWNER = 'debont80'
export const UPDATE_REPO = 'AeroFetch'
export const UPDATE_REPO = 'AeroFetch-releases'
export const RELEASE_API = `${UPDATE_HOST}/api/v1/repos/${UPDATE_OWNER}/${UPDATE_REPO}/releases/latest`
/**
* Read-only Gitea token baked in at build time so the auto-updater can read the
* PRIVATE release repo without each user pasting a token into Settings. Injected
* by electron.vite.config's `define` from the AEROFETCH_UPDATE_TOKEN env var (or a
* gitignored .update-token file) — so the real value lives only in the built
* bundle, never in source or git.
*
* SECURITY: a shipped client is decompilable, so treat this as PUBLIC. It MUST be
* a token from a dedicated service account with read-only access to ONLY this repo
* — never a personal/push-capable token. The user's own updateToken overrides it
* (see authHeader in updater.ts), so a private fork can point elsewhere.
*
* `__AEROFETCH_UPDATE_TOKEN__` is a `define`-replaced literal in built code; under
* plain vitest (no define) the identifier is undeclared, so the `typeof` guard
* keeps this from throwing and falls back to empty (no token).
*/
declare const __AEROFETCH_UPDATE_TOKEN__: string
export const BAKED_UPDATE_TOKEN: string =
typeof __AEROFETCH_UPDATE_TOKEN__ === 'string' ? __AEROFETCH_UPDATE_TOKEN__ : ''
// --- ffmpeg dependency source ------------------------------------------------
// ffmpeg/ffprobe are NOT bundled in the installer (they're the bulk of its size);
// they're fetched once on first run into the managed userData/bin dir (ffmpegSetup.ts).
//
// Unlike the app updater — which pulls from our own host-pinned release server — this
// downloads from UPSTREAM (gyan.dev's GitHub releases). Integrity therefore rests on a
// PINNED exact-version URL + SHA-256 here, re-verified against the streamed bytes: a
// swapped or tampered upstream asset fails the check and nothing is installed. Bumping
// ffmpeg is a deliberate edit to these three constants in its own commit (recompute the
// hash from the new zip) — the same "pinned decisions" discipline the app version follows.
//
// Licensing note: gyan's "essentials" is a GPL build. That's fine here because AeroFetch
// never redistributes it — it invokes ffmpeg.exe as a separate process (as yt-dlp does)
// and the user fetches the binary from upstream themselves. Swapping to an LGPL build
// (e.g. a BtbN win64 build) is purely a change to FFMPEG_ARCHIVE_URL/SHA-256 below,
// provided the zip still carries ffmpeg.exe + ffprobe.exe under a `*/bin/` path.
export const FFMPEG_VERSION = '8.1.2'
export const FFMPEG_ARCHIVE_URL = `https://github.com/GyanD/codexffmpeg/releases/download/${FFMPEG_VERSION}/ffmpeg-${FFMPEG_VERSION}-essentials_build.zip`
export const FFMPEG_ARCHIVE_SHA256 =
'db580001caa24ac104c8cb856cd113a87b0a443f7bdf47d8c12b1d740584a2ec'
// The setup UI quotes ~105 MB for this archive; keep that copy in sync when bumping above.
// Hosts the archive download may touch, HTTPS-only, re-checked on EVERY redirect hop
// (github.com 302s a release asset to release-assets.githubusercontent.com; the older
// objects.githubusercontent.com is kept for resilience). Anything off this list fails safe.
export const FFMPEG_TRUSTED_HOSTS = [
'github.com',
'release-assets.githubusercontent.com',
'objects.githubusercontent.com'
] as const