fix(main): L136 — enforce incognito in main (no errorlog / toast / cookie leak)
Incognito lived only in the renderer (history skip), so a "private" download still leaked in main: errorlog entries (title+URL), OS toasts showing the title, and the signed-in cookies attached — the UI promised "no logging, no history, no cookies" but only history held. Plumb `incognito` through StartDownloadOptions and enforce it in main: - logFailure skips the errorlog write (and the pre-spawn errorlog in ipc.ts). - notify uses a generic outcome title and drops the detail body, so no title/URL lands in the Windows Action Center. - both cookie sources (--cookies login jar, --cookies-from-browser) are withheld, so a private download can't be tied to the user's identity. typecheck + 262 tests + eslint + prettier green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+8
-2
@@ -755,9 +755,15 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
|||||||
|
|
||||||
*Round 6 (2026-06-29) — behavioral/logic edge cases:*
|
*Round 6 (2026-06-29) — behavioral/logic edge cases:*
|
||||||
|
|
||||||
- [ ] **L136 — Incognito leaks outside history.** Even if the (unreachable, M6) private flag were set,
|
- [x] **L136 — Incognito leaks outside history.** Even if the (unreachable, M6) private flag were set,
|
||||||
`download.ts` still writes failures to `errorlog.json` (title + URL) and shows the title in completion
|
`download.ts` still writes failures to `errorlog.json` (title + URL) and shows the title in completion
|
||||||
notifications — the "private" promise covers only history.
|
notifications — the "private" promise covers only history. *Fixed: `incognito` now flows through
|
||||||
|
`StartDownloadOptions` to main (the renderer's launch path passes `item.incognito`), where main enforces
|
||||||
|
the full "no logging, no history, no cookies" promise: `logFailure` skips the errorlog write (and the
|
||||||
|
pre-spawn errorlog in [ipc.ts](src/main/ipc.ts) too), `notify` uses a generic outcome title and drops the
|
||||||
|
detail body so no title/URL lands in the Windows Action Center, and both cookie sources
|
||||||
|
(`--cookies`/`--cookies-from-browser`) are withheld so a private download can't be tied to the signed-in
|
||||||
|
identity. (M6 wired the toggle; this makes the promise true in main, not just the renderer.)*
|
||||||
- [x] **L137 — Stuck 0% for unknown-size downloads.** `parseProgress` returns `progress: 0` when
|
- [x] **L137 — Stuck 0% for unknown-size downloads.** `parseProgress` returns `progress: 0` when
|
||||||
`total_bytes`/`_estimate` are absent (livestreams, some sites), so the bar reads 0% the whole time
|
`total_bytes`/`_estimate` are absent (livestreams, some sites), so the bar reads 0% the whole time
|
||||||
instead of an indeterminate state. *Fixed: `parseProgress` now sets `sizeUnknown: !totalBytes` on
|
instead of an indeterminate state. *Fixed: `parseProgress` now sets `sizeUnknown: !totalBytes` on
|
||||||
|
|||||||
+38
-8
@@ -90,9 +90,12 @@ function send(wc: WebContents, ev: DownloadEvent): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Native OS notification on completion/failure, gated by Settings.notifyOnComplete. */
|
/** Native OS notification on completion/failure, gated by Settings.notifyOnComplete. */
|
||||||
function notify(wc: WebContents, title: string, body: string): void {
|
function notify(wc: WebContents, title: string, body: string, incognito = false): void {
|
||||||
if (!getSettings().notifyOnComplete || !Notification.isSupported()) return
|
if (!getSettings().notifyOnComplete || !Notification.isSupported()) return
|
||||||
const n = new Notification({ title, body, icon: getAppIconImage() })
|
// L136: an incognito ("private") download must not leak its title/URL into an OS
|
||||||
|
// toast (it persists in the Windows Action Center). Callers pass a generic outcome
|
||||||
|
// title for the private case; here we additionally drop the detail body.
|
||||||
|
const n = new Notification({ title, body: incognito ? '' : body, icon: getAppIconImage() })
|
||||||
n.on('click', () => {
|
n.on('click', () => {
|
||||||
if (wc.isDestroyed()) return
|
if (wc.isDestroyed()) return
|
||||||
const win = BrowserWindow.fromWebContents(wc)
|
const win = BrowserWindow.fromWebContents(wc)
|
||||||
@@ -106,6 +109,9 @@ function notify(wc: WebContents, title: string, body: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function logFailure(opts: StartDownloadOptions, title: string | undefined, error: string): void {
|
function logFailure(opts: StartDownloadOptions, title: string | undefined, error: string): void {
|
||||||
|
// L136: incognito downloads are never recorded — not even a failure entry, which
|
||||||
|
// would persist the title + URL in the user-facing diagnostics log.
|
||||||
|
if (opts.incognito) return
|
||||||
addErrorLog({
|
addErrorLog({
|
||||||
id: opts.id,
|
id: opts.id,
|
||||||
title,
|
title,
|
||||||
@@ -199,7 +205,9 @@ export function buildCommand(opts: StartDownloadOptions, cookiesFile?: string):
|
|||||||
proxy: settings.proxy,
|
proxy: settings.proxy,
|
||||||
rateLimit: settings.rateLimit,
|
rateLimit: settings.rateLimit,
|
||||||
aria2cPath,
|
aria2cPath,
|
||||||
cookiesFromBrowser: settings.cookieSource === 'browser' ? settings.cookiesBrowser : undefined,
|
// L136/M6: incognito attaches no cookies from the browser either.
|
||||||
|
cookiesFromBrowser:
|
||||||
|
!opts.incognito && settings.cookieSource === 'browser' ? settings.cookiesBrowser : undefined,
|
||||||
cookiesFile,
|
cookiesFile,
|
||||||
restrictFilenames: settings.restrictFilenames,
|
restrictFilenames: settings.restrictFilenames,
|
||||||
downloadArchivePath: settings.downloadArchive ? getDownloadArchivePath() : undefined,
|
downloadArchivePath: settings.downloadArchive ? getDownloadArchivePath() : undefined,
|
||||||
@@ -279,7 +287,9 @@ export function startDownload(wc: WebContents, opts: StartDownloadOptions): Star
|
|||||||
// yt-dlp reads --cookies once at startup; we delete it the moment the download
|
// yt-dlp reads --cookies once at startup; we delete it the moment the download
|
||||||
// settles, so the plaintext never lingers at rest.
|
// settles, so the plaintext never lingers at rest.
|
||||||
let cookiesFile: string | undefined
|
let cookiesFile: string | undefined
|
||||||
if (getSettings().cookieSource === 'login' && hasStoredCookies()) {
|
// L136/M6: an incognito download attaches no saved login cookies (the UI promises
|
||||||
|
// "no cookies"), so it can't be tied to the user's signed-in identity.
|
||||||
|
if (!opts.incognito && getSettings().cookieSource === 'login' && hasStoredCookies()) {
|
||||||
// Unique per spawn (not just per id): a retry/resume reuses opts.id, and the
|
// Unique per spawn (not just per id): a retry/resume reuses opts.id, and the
|
||||||
// superseded download's cleanupCookies() must not unlink the new spawn's jar.
|
// superseded download's cleanupCookies() must not unlink the new spawn's jar.
|
||||||
const tmp = join(tmpdir(), `aerofetch-cookies-${opts.id}-${++spawnSeq}.txt`)
|
const tmp = join(tmpdir(), `aerofetch-cookies-${opts.id}-${++spawnSeq}.txt`)
|
||||||
@@ -390,7 +400,12 @@ function wireChildProcess(params: {
|
|||||||
const msg = `Download stalled — no activity for ${Math.round(STALL_TIMEOUT_MS / 60_000)} min. Stopped; retry to resume.`
|
const msg = `Download stalled — no activity for ${Math.round(STALL_TIMEOUT_MS / 60_000)} min. Stopped; retry to resume.`
|
||||||
send(wc, { type: 'error', id: opts.id, error: msg })
|
send(wc, { type: 'error', id: opts.id, error: msg })
|
||||||
logFailure(opts, getTitle(), msg)
|
logFailure(opts, getTitle(), msg)
|
||||||
notify(wc, getTitle() ?? 'Download failed', msg)
|
notify(
|
||||||
|
wc,
|
||||||
|
opts.incognito ? 'Download stopped' : (getTitle() ?? 'Download failed'),
|
||||||
|
msg,
|
||||||
|
opts.incognito
|
||||||
|
)
|
||||||
}, STALL_TIMEOUT_MS)
|
}, STALL_TIMEOUT_MS)
|
||||||
}
|
}
|
||||||
bumpWatchdog()
|
bumpWatchdog()
|
||||||
@@ -434,7 +449,12 @@ function wireChildProcess(params: {
|
|||||||
if (!rec.canceled && !rec.paused) {
|
if (!rec.canceled && !rec.paused) {
|
||||||
send(wc, { type: 'error', id: opts.id, error: err.message })
|
send(wc, { type: 'error', id: opts.id, error: err.message })
|
||||||
logFailure(opts, getTitle(), err.message)
|
logFailure(opts, getTitle(), err.message)
|
||||||
notify(wc, getTitle() ?? 'Download failed', err.message)
|
notify(
|
||||||
|
wc,
|
||||||
|
opts.incognito ? 'Download failed' : (getTitle() ?? 'Download failed'),
|
||||||
|
err.message,
|
||||||
|
opts.incognito
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -449,12 +469,22 @@ function wireChildProcess(params: {
|
|||||||
if (rec.canceled || rec.paused) return
|
if (rec.canceled || rec.paused) return
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
send(wc, { type: 'done', id: opts.id, filePath })
|
send(wc, { type: 'done', id: opts.id, filePath })
|
||||||
notify(wc, getTitle() ?? 'Download complete', 'Finished downloading.')
|
notify(
|
||||||
|
wc,
|
||||||
|
opts.incognito ? 'Download complete' : (getTitle() ?? 'Download complete'),
|
||||||
|
'Finished downloading.',
|
||||||
|
opts.incognito
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
const msg = cleanError(stderrTail) || `yt-dlp exited with code ${code}`
|
const msg = cleanError(stderrTail) || `yt-dlp exited with code ${code}`
|
||||||
send(wc, { type: 'error', id: opts.id, error: msg })
|
send(wc, { type: 'error', id: opts.id, error: msg })
|
||||||
logFailure(opts, getTitle(), msg)
|
logFailure(opts, getTitle(), msg)
|
||||||
notify(wc, getTitle() ?? 'Download failed', msg)
|
notify(
|
||||||
|
wc,
|
||||||
|
opts.incognito ? 'Download failed' : (getTitle() ?? 'Download failed'),
|
||||||
|
msg,
|
||||||
|
opts.incognito
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -111,8 +111,9 @@ export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null):
|
|||||||
ipcMain.handle(IpcChannels.downloadStart, (e, opts: StartDownloadOptions) => {
|
ipcMain.handle(IpcChannels.downloadStart, (e, opts: StartDownloadOptions) => {
|
||||||
const result = startDownload(e.sender, opts)
|
const result = startDownload(e.sender, opts)
|
||||||
// Pre-spawn failures (missing yt-dlp.exe, bad URL, duplicate id) never reach
|
// Pre-spawn failures (missing yt-dlp.exe, bad URL, duplicate id) never reach
|
||||||
// download.ts's own close/error handlers, so log them here instead.
|
// download.ts's own close/error handlers, so log them here instead — unless the
|
||||||
if (!result.ok) {
|
// download is incognito, which is never recorded (L136).
|
||||||
|
if (!result.ok && !opts.incognito) {
|
||||||
addErrorLog({
|
addErrorLog({
|
||||||
id: opts.id,
|
id: opts.id,
|
||||||
url: opts.url,
|
url: opts.url,
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ export const useDownloads = create<DownloadState>((set, get) => {
|
|||||||
options: item.options,
|
options: item.options,
|
||||||
extraArgs: item.extraArgs,
|
extraArgs: item.extraArgs,
|
||||||
trim: item.trim,
|
trim: item.trim,
|
||||||
|
// Carry the private flag to main so it also skips errorlog, notification
|
||||||
|
// detail, and cookies — not just the renderer's history skip (L136).
|
||||||
|
incognito: item.incognito,
|
||||||
meta: item.probedMeta,
|
meta: item.probedMeta,
|
||||||
collection: item.collection
|
collection: item.collection
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -423,6 +423,13 @@ export interface StartDownloadOptions {
|
|||||||
formatId?: string
|
formatId?: string
|
||||||
/** whether the chosen format already includes audio (skips +bestaudio) */
|
/** whether the chosen format already includes audio (skips +bestaudio) */
|
||||||
formatHasAudio?: boolean
|
formatHasAudio?: boolean
|
||||||
|
/**
|
||||||
|
* Incognito ("private") download. Main enforces the promise the UI makes ("no
|
||||||
|
* logging, no history, no cookies"): it writes no errorlog entry, reveals no
|
||||||
|
* title/URL in an OS notification, and attaches no saved login / browser
|
||||||
|
* cookies — not just the renderer's history skip (L136 / M6).
|
||||||
|
*/
|
||||||
|
incognito?: boolean
|
||||||
/** per-download post-processing override; main falls back to settings defaults */
|
/** per-download post-processing override; main falls back to settings defaults */
|
||||||
options?: DownloadOptions
|
options?: DownloadOptions
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user