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>
This commit is contained in:
2026-06-29 10:15:59 -04:00
parent f167c02946
commit 5a9f2de390
8 changed files with 278 additions and 100 deletions
+10 -2
View File
@@ -15,8 +15,12 @@ import {
* Authorization header for the update host. Empty unless the user has set an
* updateToken in Settings — needed when the release repo is private or the Gitea
* instance requires sign-in for anonymous access (the default on this instance).
* Only ever sent to the host-pinned UPDATE_HOST (see isTrustedDownloadUrl), so a
* redirect can never leak the token to another origin.
*
* The token must never reach an origin other than the host-pinned UPDATE_HOST.
* Every request that carries it guards against that on its own terms: the REST
* check refuses to follow redirects (`redirect: 'error'`), and the download paths
* use `redirect: 'manual'` and re-validate each hop with isTrustedDownloadUrl. So
* a redirect can't bounce the header to another host on any path.
*/
function authHeader(): Record<string, string> {
const tok = getSettings().updateToken?.trim()
@@ -123,6 +127,10 @@ export async function checkForAppUpdate(): Promise<AppUpdateInfo> {
try {
const res = await fetch(RELEASE_API, {
headers: { Accept: 'application/json', ...authHeader() },
// The API lives at an exact pinned URL and answers 200 directly. Refusing
// redirects keeps an authenticated check from ever forwarding the token to
// another origin; a stray redirect fails safe into the catch below.
redirect: 'error',
signal: controller.signal
})
if (!res.ok) {