Addressing issues found reviewing this session's audit commits:
REGRESSIONS FIXED
- updater.ts (L52): reverted the spawn(detached) installer launch. Raw
CreateProcess/spawn fails with ERROR_ELEVATION_REQUIRED if the NSIS build
flips to perMachine (the config comment invites this), and it silently
dropped shell.openPath's launch-error detection. Restored shell.openPath
(ShellExecute honors the elevation manifest) and kept the L52 goal by
replacing the 1500ms timer with setImmediate(app.quit).
- LibraryView.tsx (M15): native <button> doesn't inherit color (UA sets
ButtonText), so the source-card chevron (currentColor) would render wrong
in dark mode. Added color: colorNeutralForeground1 to cardHead.
INEFFECTIVE CHANGE REVERTED
- base.css (W18): forced-color-adjust:auto on * is the CSS default (no-op),
and [class*="backdrop"] never matches Fluent/Griffel's hashed atomic class
names. Reverted; W18 unmarked (needs real High-Contrast testing).
WEAK FIX REVERTED
- Onboarding.tsx (L67): "Skip" and "Get started" called the identical handler
on a single-screen onboarding. Removed the duplicate button; L67 unmarked
(real ask is a "show tips again" revisit affordance).
STYLE / CORRECTNESS
- Stripped UTF-8 BOMs accidentally added to Select/CommandPalette/App/
Onboarding by the PowerShell sanitizer; left pre-existing BOMs untouched.
- DownloadBar.tsx: merged the duplicate @shared/ipc import; removed a stray
double blank line.
- qualityOptions.ts (L29): restored the original `satisfies Record<MediaKind,
readonly string[]>` constraint + the noUncheckedIndexedAccess rationale
comment (the extraction had dropped both for a weaker `as const`).
- downloads.ts: removed double blank line left by the QUALITY_OPTIONS move.
- binaries.ts: corrected YTDLP_MISSING_MSG doc comment ("at startup" -> the
actual call sites).
typecheck + 242 tests + eslint all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L29: QUALITY_OPTIONS was a presentational constant living in store/downloads.ts,
coupling views to the store. Moved to src/renderer/src/qualityOptions.ts,
which assembles it directly from @shared/ipc. store/downloads.ts no longer
imports VIDEO/AUDIO_QUALITY_OPTIONS. Both consumers (DownloadBar, SettingsView)
updated to import from the new module.
L52: Replaced the fixed `setTimeout(app.quit, 1500)` installer handoff with a
detached spawn. The installer is spawned with stdio:'ignore' and unref'd so
it outlives AeroFetch even on a slow machine. app.quit() fires on setImmediate
so the IPC response returns to the renderer before the process exits.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Encrypt proxy / youtubePoToken / updateToken on disk via safeStorage (DPAPI on Windows), with a one-time launch migration for legacy plaintext. Decrypted before reaching callers/renderer; backup export still writes clear, as documented. Harden updater token handling to refuse cross-origin redirects on the authenticated REST check.
Extract the clipboard-link logic from DownloadBar into the shared useClipboardLink hook: add an optional filter (library skips single-video links), an offer() for external aerofetch:// / .url links, and a source field driving banner wording. Add looksLikeSingleVideo plus its unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closing the window no longer kills in-progress downloads, the library gains the
same copied-link suggestion the downloads tab has, and the in-app updater can now
authenticate to a sign-in-required Gitea.
Background / tray:
- The window's close handler now hides to the tray (instead of quitting) whenever
a download is in flight, even if "Keep running in the tray" is off — quitting
was killing the spawned yt-dlp processes. A one-time notification explains the
app is still running. (download.ts exposes hasActiveDownloads().)
- tray.ts now falls back to an embedded icon when no build/icon.ico ships, so the
tray actually appears — previously an empty icon meant the tray was skipped and
a minimized window could be stranded with no way back.
- New "Start with Windows" setting (launchAtStartup) wired to
app.setLoginItemSettings, synced at startup and on toggle. Useful with
auto-download so watched channels stay current in the background.
- The "Keep running in the tray" hint now explains it also enables background
auto-download of new uploads.
Library clipboard detection:
- Extracted the downloads tab's clipboard watcher into a shared useClipboardLink
hook and used it in the library's add-source field, so a copied channel/playlist
link is offered there too.
Updater fix (works on a private / sign-in-required instance):
- Added an optional updateToken setting. When set, the updater sends it as a Gitea
Authorization header on the release check, the checksum fetch, and the installer
download — so "Check for updates" works where anonymous access is blocked (the
previous "could not reach the update server" case). Blank = anonymous, unchanged.
No token is ever shipped; it's only ever sent to the host-pinned update host.
Settings gains a masked "Update access token" field.
Typecheck clean; 187 tests pass; electron-vite build clean. New UI verified in the
browser preview (tray/startup toggles, token field, library copied-link banner).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>