Commit Graph

12 Commits

Author SHA1 Message Date
debont80 9134e7d216 feat(audit): Batch 25 — project reorg (CC12), leading DI args (CC7), CC10 by-design
CC12: renderer views/ (5 screens + Onboarding + settings cards) +
lib/ (theme/thumb/datetime/id/qualityOptions/useClipboardLink/queueStats);
main core/ (buildArgs/validation/indexerCore/ytdlpPolicy). git mv preserved
history; all src+test imports updated. Build emits view chunks from views/,
344 tests + typecheck green, live probe rendered all screens.
CC7: wc/onProgress is the leading arg everywhere — downloadAppUpdate(wc,url),
indexSource(onProgress,url,signal), indexSourceCancelable(onProgress,url).
CC10: closed by-design — jsonStore backs all records; settings stay in
electron-store for DPAPI secret encryption (a deliberate two-store split).
Also closes the UI24/W4 context-menu cross-reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 15:37:28 -04:00
debont80 134c6a167c feat(audit): Batch 23 — settings key renames with migration (CC1) + config.ts (CC11)
CC1: customCommandEnabled→enableCustomCommands, downloadArchive→
useDownloadArchive, clipboardWatch→watchClipboard, sidebarCollapsed→
isSidebarCollapsed, hardwareAcceleration→useHardwareAcceleration,
videoDir/audioDir→videoFolder/audioFolder (+ chooseDir/clearDir store
actions → chooseFolder/clearFolder). Rename map in settingsMigration.ts
(pure, unit-tested incl. pre-rename backup fixture), applied as an
idempotent on-disk shim at store open and on backup import so old
settings.json and old backups both keep working.

CC11: update host/owner/repo + release API moved to src/main/config.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:34:05 -04:00
debont80 34fbf36610 feat(audit): Batch 21 — updater refactor under behavior pins (CL4, closes CC5)
14 pinning tests lock downloadAppUpdate's security invariants (redirect
re-validation per hop, checksum refusal paths, SHA-256 verify + cleanup,
truncation, idle-timeout, cancel in both phases, progress) with a
scripted net.request fake; then the nested streaming block is extracted
verbatim into streamInstallerToFile. downloadAppUpdate is now a linear
pre-flight; B6 cancel-slot ownership stays with its module state via
onCancelReady/onSettled. Pins green before and after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:21:28 -04:00
debont80 e19f988c72 feat(audit): Batch 15 live-verify — download-engine lifecycle (R4, L65, L139, B2, B6, L143)
The watched live session for the six deferred lifecycle items, each verified
against real yt-dlp/Electron behavior on Windows:

- R4: cancel now deletes the download's orphaned partials. The engine captures
  the final output path via a new `--print before_dl:dest|%(filename)s`
  (empirically %(filepath)s is still 'NA' that early) and the close handler
  sweeps only unambiguous intermediates (.part / .part-Frag* / .ytdl /
  <stem>.fNNN.<ext>) via the pure, unit-tested lib/partials.ts — never a bare
  <stem>.<ext> or another download's files. Live-verified with decoys.
- L65: verified-acceptable, no code change — a taskkill /F pause leaves the
  .part byte-intact and yt-dlp --continue resumes at the exact byte offset.
- L139: spawn↔self-update interlock (new ytdlpLock.ts). The updater refuses
  while any yt-dlp spawn is live; download/terminal IPC handlers hold (await)
  behind an in-progress exe rewrite instead of failing.
- B2: source indexing is cancellable — AbortSignal through the probe walk
  (kills the in-flight child, live-verified via tasklist), sources:index-cancel
  IPC, and a Cancel button in the Library add-source row.
- B6: app-update download is cancellable — app:update-cancel routes through
  the existing finish() teardown (abort + destroy + unlink, live-verified on
  Electron net.request); Cancel button under the update progress bar. The
  checksum phase is cancelable too.
- L143: closing the window mid-download (non-tray mode) now offers a native
  tray-independent "Quit anyway / Keep downloading" dialog, replacing the
  passive "use the tray to quit" notification.

Peer-review pass caught and fixed: a non-reentrant update lock (concurrent
begin clobbered the settle promise), a dead Cancel window during the B6
checksum fetch + a canceller slot-ownership bug (L140 shape), and a
persist-after-cancel hole in the index walk.

typecheck + 304 tests + eslint + production build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:36:22 -04:00
debont80 d7b5737806 Self-review fixes: revert ineffective/regression-risk changes from audit batch
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>
2026-06-30 15:11:13 -04:00
debont80 a4fd0feb6b Fix L29/L52: QUALITY_OPTIONS moved to UI module; installer spawn via child_process
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>
2026-06-30 14:32:50 -04:00
debont80 1376c2dee8 Harden audit findings: correctness, type-safety, Windows conventions & polish
Audit-pass over CODE-AUDIT.md (~48 items closed this pass; all verified —
typecheck + 234 tests + eslint + prettier green).

Correctness / bugs:
- B3: match the release checksum to the asset's filename line (no wrong-hash verify)
- B4: newline-safe metadata probe (one --print with a unit-separator delimiter)
- B5 / L88: guard the meta event against canceled items; progress no longer promotes
  a queued item outside pump()
- B7: cookie-login promise always resolves (handles destroy-without-close)
- L146: trim parser rejects >2 colon-group times; M36: Library selection counts only
  actionable rows
- L11 / L50 / L156 / L57 / L159 / L15 / L3: live queue count, empty-cookie message,
  schedule picker min, dead-code/comment cleanup

Type safety:
- Enable noUncheckedIndexedAccess + noFallthroughCasesInSwitch (15 real edge cases fixed)

Resilience / Windows / metadata:
- R5: settings write failure handled (no unhandled IPC rejection; reconciles to truth)
- W1 / W5 / W6: min window size, seeded folder picker, parented sign-in window;
  L147 dead macOS branches removed
- CL1: shared stdout markers; package/builder metadata (license, homepage, repository,
  copyright, tsbuildinfo glob)

Copy / docs / tests:
- M37 / SR9 dev-jargon cleanup in hints; M8 / M25 / M26 / L66 / L80 / L81 reconciled
- New unit tests for L35 (isValidMediaItem) and L36 (compareVersions)

This commit also checkpoints the previously-uncommitted feat/tray-background-clipboard
work it builds on: background running + auto-download, library clipboard detection,
tray, binary management & library scale, credential encryption at rest, the shared
jsonStore and ui/ primitives, and the eslint/prettier tooling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:02:54 -04:00
debont80 5a9f2de390 Encrypt credentials at rest; unify clipboard-link suggestions
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>
2026-06-29 10:16:16 -04:00
wayne f167c02946 Background running for downloads/auto-download, library clipboard detect, updater auth + token
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>
2026-06-28 12:22:19 -04:00
debont80 fa92383ef4 Verify update installer SHA-256; harden download teardown
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>
2026-06-25 05:38:02 -04:00
debont80 7134a3d634 Harden app updater: per-hop redirect pinning, truncation + path-guard fixes
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>
2026-06-25 05:06:01 -04:00
wayne 981d2dd34d Add in-app updater: check Gitea releases, show changelog, download + run installer
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>
2026-06-24 22:38:35 -04:00