diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml
index 83aa145..9385414 100644
--- a/.github/workflows/test-all.yml
+++ b/.github/workflows/test-all.yml
@@ -10,6 +10,11 @@ on:
permissions:
contents: read
+# Avoid burning the 3-OS matrix on superseded pushes/PR updates.
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
env:
NODE_VERSION: '24'
@@ -123,3 +128,41 @@ jobs:
- name: Dependency Review
uses: actions/dependency-review-action@v5
+
+ coverage:
+ name: Coverage thresholds
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run coverage and enforce per-file thresholds
+ run: npm run test:cov
+
+ audit:
+ name: npm audit
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Audit dependencies (fails on high/critical)
+ run: npm audit --audit-level=high
diff --git a/.gitignore b/.gitignore
index 294e3cb..3d4e095 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,3 +58,6 @@ resources/ffmpeg/**/ffplay.exe
# REPL history
.node_repl_history
+
+# Test scratch dirs
+.tmp-*/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c04051..d6ab9bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,9 +5,9 @@
| Windows |
macOS |
Linux |
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **EXE:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-Windows-x64.exe) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-Windows-arm64.exe) | **[Universal DMG](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-MacOS-universal.dmg)** | **AppImage:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-Linux-x86_64.AppImage) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-Linux-arm64.AppImage) |
-|
+
+ROSI invokes yt-dlp as an external process and is not a derivative work of
+yt-dlp or any of its bundled components. ROSI is licensed under the Mozilla
+Public License 2.0 (MPL-2.0).
diff --git a/assets/ytdlp-checksums.json b/assets/ytdlp-checksums.json
new file mode 100644
index 0000000..67d7e0f
--- /dev/null
+++ b/assets/ytdlp-checksums.json
@@ -0,0 +1,11 @@
+{
+ "_comment": "SHA-256 of the committed yt-dlp binaries. Regenerate ONLY after verifying the binaries against yt-dlp upstream SHA2-256SUMS. See build-scripts/check-ytdlp.js.",
+ "generatedAt": "2026-06-17T21:25:31.250Z",
+ "binaries": {
+ "yt-dlp.exe": "3a48cb955d55c8821b60ccbdbbc6f61bc958f2f3d3b7ad5eaf3d83a543293a27",
+ "yt-dlp_arm64.exe": "847583f91bb6d26479c1dc9643c2f4b8857a90b40d619da97b0cfabccb9138d0",
+ "yt-dlp_macos": "b82c3626952e6c14eaf654cc565866775ffd0b9ffb7021628ac59b42c2f4f244",
+ "yt-dlp_linux": "bf8aac79b72287a6d2043074415132558b43743a8f9461a22b0141e90f16ce66",
+ "yt-dlp_linux_aarch64": "cabd246445bdfde0eda0dfe68bbe90354be83f3fdbbf077df11a2ea55f41cdbd"
+ }
+}
diff --git a/build-scripts/check-coverage-thresholds.js b/build-scripts/check-coverage-thresholds.js
index 9e2cf62..70b0e48 100644
--- a/build-scripts/check-coverage-thresholds.js
+++ b/build-scripts/check-coverage-thresholds.js
@@ -10,14 +10,37 @@ if (!fs.existsSync(summaryPath)) {
const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
+// Per-file floors for the security-critical / non-trivial main-process and
+// shared modules. Each is set a few points below the currently-measured
+// coverage so the gate catches regressions without being flaky. All four
+// metrics (lines/statements/branches/functions) are enforced.
+//
+// NOTE: renderer modules (rosiEngine.ts, modules/*.ts) are intentionally absent.
+// They are exercised via on-the-fly transpile+eval in the test suite, which v8
+// coverage cannot attribute to the source files, so they report 0% here. Add
+// floors for them only once the renderer tests import the compiled artifact.
const thresholds = {
- 'src/main/main.ts': { lines: 12, statements: 12 },
- 'src/main/preload.ts': { lines: 80, statements: 80 },
- 'src/main/processKill.ts': { lines: 80, statements: 80 },
- 'src/utils/validation.ts': { lines: 85, statements: 85 },
- 'src/utils/downloadLifecycle.ts': { lines: 90, statements: 90 },
+ 'src/main/main.ts': { lines: 80, statements: 80, branches: 70, functions: 80 },
+ 'src/main/downloader.ts': { lines: 88, statements: 88, branches: 72, functions: 88 },
+ 'src/main/platform.ts': { lines: 68, statements: 68, branches: 58, functions: 58 },
+ 'src/main/settings.ts': { lines: 88, statements: 88, branches: 82, functions: 90 },
+ 'src/main/updater.ts': { lines: 92, statements: 92, branches: 85, functions: 92 },
+ 'src/main/download/commandBuilders.ts': {
+ lines: 88,
+ statements: 88,
+ branches: 86,
+ functions: 90,
+ },
+ 'src/main/download/videoInfo.ts': { lines: 70, statements: 70, branches: 60, functions: 70 },
+ 'src/main/preload.ts': { lines: 90, statements: 90, branches: 90, functions: 90 },
+ 'src/main/processKill.ts': { lines: 90, statements: 90, branches: 85, functions: 55 },
+ 'src/utils/ipcValidation.ts': { lines: 85, statements: 85, branches: 85, functions: 88 },
+ 'src/utils/validation.ts': { lines: 85, statements: 82, branches: 72, functions: 90 },
+ 'src/utils/downloadLifecycle.ts': { lines: 95, statements: 95, branches: 95, functions: 95 },
};
+const METRICS = ['lines', 'statements', 'branches', 'functions'];
+
function findCoverageEntry(suffix) {
const normalizedSuffix = suffix.replace(/\\/g, '/');
return Object.entries(summary).find(([key]) =>
@@ -35,11 +58,13 @@ for (const [file, threshold] of Object.entries(thresholds)) {
}
const [, metrics] = match;
- if (metrics.lines.pct < threshold.lines) {
- failures.push(`${file}: lines ${metrics.lines.pct}% < ${threshold.lines}%`);
- }
- if (metrics.statements.pct < threshold.statements) {
- failures.push(`${file}: statements ${metrics.statements.pct}% < ${threshold.statements}%`);
+ for (const metric of METRICS) {
+ if (typeof threshold[metric] !== 'number') continue;
+ const actual =
+ metrics[metric] && typeof metrics[metric].pct === 'number' ? metrics[metric].pct : 0;
+ if (actual < threshold[metric]) {
+ failures.push(`${file}: ${metric} ${actual}% < ${threshold[metric]}%`);
+ }
}
}
diff --git a/build-scripts/check-ytdlp.js b/build-scripts/check-ytdlp.js
new file mode 100644
index 0000000..a238a81
--- /dev/null
+++ b/build-scripts/check-ytdlp.js
@@ -0,0 +1,136 @@
+'use strict';
+
+/**
+ * yt-dlp binary integrity gate.
+ *
+ * The yt-dlp binaries are committed under assets/ and shipped verbatim inside
+ * the signed installers. This script verifies each committed binary against a
+ * checksum manifest (assets/ytdlp-checksums.json) so a corrupted, truncated, or
+ * tampered binary cannot be packaged unnoticed.
+ *
+ * Usage:
+ * node build-scripts/check-ytdlp.js verify (default; fails on mismatch/missing manifest)
+ * node build-scripts/check-ytdlp.js --generate (re)write the manifest from the current binaries
+ *
+ * NOTE ON PROVENANCE: --generate records the hashes of whatever binaries are
+ * currently on disk (a self-attested baseline that detects later drift). It does
+ * NOT prove the binaries match an upstream yt-dlp release. When updating yt-dlp,
+ * download the official binaries, verify them against yt-dlp's published
+ * SHA2-256SUMS (and its GPG signature), then run --generate to record the new
+ * baseline.
+ */
+
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+
+const assetsDir = path.join(__dirname, '..', 'assets');
+const manifestPath = path.join(assetsDir, 'ytdlp-checksums.json');
+
+// All per-platform yt-dlp binaries ROSI ships (mirrors getYtdlpBinaryName()).
+const BINARY_NAMES = [
+ 'yt-dlp.exe',
+ 'yt-dlp_arm64.exe',
+ 'yt-dlp_macos',
+ 'yt-dlp_linux',
+ 'yt-dlp_linux_aarch64',
+];
+
+function sha256(filePath) {
+ const hash = crypto.createHash('sha256');
+ hash.update(fs.readFileSync(filePath));
+ return hash.digest('hex');
+}
+
+function presentBinaries() {
+ return BINARY_NAMES.filter((name) => fs.existsSync(path.join(assetsDir, name)));
+}
+
+function generate() {
+ const present = presentBinaries();
+ if (present.length === 0) {
+ console.error('✗ No yt-dlp binaries found in assets/; nothing to record.');
+ process.exit(1);
+ }
+ const binaries = {};
+ for (const name of present) {
+ binaries[name] = sha256(path.join(assetsDir, name));
+ }
+ const manifest = {
+ _comment:
+ 'SHA-256 of the committed yt-dlp binaries. Regenerate ONLY after verifying ' +
+ 'the binaries against yt-dlp upstream SHA2-256SUMS. See build-scripts/check-ytdlp.js.',
+ generatedAt: new Date().toISOString(),
+ binaries,
+ };
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
+ console.log(`✓ Wrote ${path.basename(manifestPath)} for ${present.length} binaries.`);
+}
+
+function verify() {
+ if (!fs.existsSync(manifestPath)) {
+ console.error(`✗ yt-dlp checksum manifest not found: ${manifestPath}`);
+ console.error(' Generate it with: node build-scripts/check-ytdlp.js --generate');
+ process.exit(1);
+ }
+
+ let manifest;
+ try {
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
+ } catch (error) {
+ console.error(`✗ Could not parse ${manifestPath}: ${error.message}`);
+ process.exit(1);
+ }
+
+ const expected = (manifest && manifest.binaries) || {};
+ const errors = [];
+ const present = presentBinaries();
+
+ if (present.length === 0) {
+ console.error('✗ No yt-dlp binaries found in assets/.');
+ process.exit(1);
+ }
+
+ for (const name of present) {
+ const expectedHash = expected[name];
+ if (!expectedHash) {
+ errors.push(`${name}: present on disk but missing from the manifest`);
+ continue;
+ }
+ const actual = sha256(path.join(assetsDir, name));
+ if (actual !== expectedHash) {
+ errors.push(
+ `${name}: SHA-256 mismatch\n expected: ${expectedHash}\n actual: ${actual}`
+ );
+ }
+ }
+
+ // A manifest entry without a binary on disk is only an error when that binary
+ // is required for the current build; here we just warn so single-arch checkouts
+ // (where other-arch binaries were temporarily removed) do not fail.
+ for (const name of Object.keys(expected)) {
+ if (!present.includes(name)) {
+ console.warn(` (note) ${name} is in the manifest but not present on disk.`);
+ }
+ }
+
+ if (errors.length > 0) {
+ console.error('\n✗ yt-dlp integrity check failed:');
+ for (const item of errors) {
+ console.error(`- ${item}`);
+ }
+ console.error(
+ '\nIf you intentionally updated yt-dlp, verify the new binaries against upstream\n' +
+ 'SHA2-256SUMS, then run: node build-scripts/check-ytdlp.js --generate'
+ );
+ process.exit(1);
+ }
+
+ console.log(`✓ yt-dlp integrity verified (${present.length} binaries).`);
+}
+
+if (process.argv.includes('--generate')) {
+ generate();
+} else {
+ verify();
+}
diff --git a/build-scripts/dist-tools.js b/build-scripts/dist-tools.js
index 7d70fa7..2e4c6ae 100644
--- a/build-scripts/dist-tools.js
+++ b/build-scripts/dist-tools.js
@@ -48,7 +48,7 @@ function cleanReleaseArtifacts() {
}
function cleanRendererModuleArtifacts() {
- let entries = [];
+ let entries;
try {
entries = fs.readdirSync(RENDERER_MODULES_DIR, { withFileTypes: true });
} catch (error) {
diff --git a/build-scripts/gpg-sign.js b/build-scripts/gpg-sign.js
index 42bf6f4..be368a1 100644
--- a/build-scripts/gpg-sign.js
+++ b/build-scripts/gpg-sign.js
@@ -117,17 +117,25 @@ function signFile(filePath) {
gpgArgs.push('--local-user', GPG_KEY_ID);
}
+ // Pass the passphrase over stdin (--passphrase-fd 0) instead of argv so it
+ // never appears in the process table or in any error.message (which, for
+ // execFileSync, embeds the full argv).
+ const execOptions = { stdio: ['pipe', 'pipe', 'pipe'] };
if (GPG_PASSPHRASE) {
- gpgArgs.push('--pinentry-mode', 'loopback', '--passphrase', GPG_PASSPHRASE);
+ gpgArgs.push('--pinentry-mode', 'loopback', '--passphrase-fd', '0');
+ execOptions.input = GPG_PASSPHRASE + '\n';
}
gpgArgs.push('--output', ascFile, filePath);
- execFileSync('gpg', gpgArgs, { stdio: 'pipe' });
+ execFileSync('gpg', gpgArgs, execOptions);
console.log(' ✓ Created ' + path.basename(ascFile));
return ascFile;
} catch (error) {
- console.error(' ✗ FAILED: ' + fileName + ':', error.message);
+ // Do not log error.message: it can contain the full gpg argv. Surface only
+ // the exit status so a secret on the command line can never reach the log.
+ const status = typeof error.status === 'number' ? ` (exit code ${error.status})` : '';
+ console.error(' ✗ FAILED to sign ' + fileName + status);
return null;
}
}
@@ -355,7 +363,7 @@ async function getOrCreateRelease() {
);
if (!Array.isArray(releases)) {
- throw new Error('Unexpected releases payload type');
+ throw new Error('Unexpected releases payload type', { cause: error });
}
const matchingReleases = releases.filter(function (r) {
diff --git a/build-scripts/sign-mac-helpers.js b/build-scripts/sign-mac-helpers.js
index b06d33f..9b602cb 100644
--- a/build-scripts/sign-mac-helpers.js
+++ b/build-scripts/sign-mac-helpers.js
@@ -5,7 +5,9 @@ const path = require('path');
const { execFileSync } = require('child_process');
/**
- * Re-sign bundled helper binaries after electron-builder signs the app.
+ * Sign bundled helper binaries during the afterPack hook, BEFORE electron-builder
+ * signs and seals the .app bundle. Signing nested Mach-O after the bundle is
+ * sealed would invalidate the parent signature / notarization.
* PyInstaller sidecars (yt-dlp) extract a Python runtime at launch; without
* disable-library-validation they fail with Team ID mismatches on macOS.
*/
diff --git a/build-scripts/update-metainfo.js b/build-scripts/update-metainfo.js
index e778c90..2cd1268 100644
--- a/build-scripts/update-metainfo.js
+++ b/build-scripts/update-metainfo.js
@@ -57,7 +57,6 @@ if (!releasesSectionMatch) {
}
const releaseSelfClosingRegex = /]*\/>/;
-const releasePairedRegex = /]*>[\s\S]*?<\/release>/;
const currentReleaseMatch =
releasesSectionMatch[0].match(releaseSelfClosingRegex) ||
@@ -77,17 +76,12 @@ if (currentReleaseMatch) {
}
}
-let updatedSection = releasesSectionMatch[0];
-if (releaseSelfClosingRegex.test(updatedSection)) {
- updatedSection = updatedSection.replace(releaseSelfClosingRegex, newReleaseTag);
-} else if (releasePairedRegex.test(updatedSection)) {
- updatedSection = updatedSection.replace(releasePairedRegex, newReleaseTag);
-} else {
- updatedSection = updatedSection.replace(
- /\s*/,
- `\n${newReleaseTag}\n${baseIndent}`
- );
-}
+// Prepend the new release so version history is preserved (AppStream / Flathub
+// expect a newest-first history rather than a single repeatedly-replaced entry).
+const updatedSection = releasesSectionMatch[0].replace(
+ /[^\S\r\n]*\r?\n?\s*/,
+ `\n${releaseIndent}${newReleaseTag.trim()}\n`
+);
if (updatedSection === releasesSectionMatch[0]) {
console.log('✓ AppStream metadata already up to date');
diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist
index 8999547..97cafe4 100644
--- a/build/entitlements.mac.plist
+++ b/build/entitlements.mac.plist
@@ -4,8 +4,6 @@
com.apple.security.cs.allow-jit
- com.apple.security.cs.allow-unsigned-executable-memory
-
com.apple.security.app-sandbox
diff --git a/com.burnttoasters.rosi.metainfo.xml b/com.burnttoasters.rosi.metainfo.xml
index ad5187e..8faa0a1 100644
--- a/com.burnttoasters.rosi.metainfo.xml
+++ b/com.burnttoasters.rosi.metainfo.xml
@@ -24,6 +24,7 @@
https://github.com/BurntToasters/ROSI
https://github.com/BurntToasters/ROSI/issues
+ https://github.com/BurntToasters/ROSI
BurntToasters
com.burnttoasters.rosi.desktop
@@ -33,7 +34,8 @@
-
+
+
diff --git a/com.burnttoasters.rosi.yml b/com.burnttoasters.rosi.yml
index 90d3a7f..f53e1e1 100644
--- a/com.burnttoasters.rosi.yml
+++ b/com.burnttoasters.rosi.yml
@@ -10,17 +10,17 @@ command: rosi
separate-locales: false
finish-args:
- # X11 + XShm access
+ # X11 + XShm access (only used as a fallback when Wayland is unavailable)
- --share=ipc
- - --socket=x11
+ - --socket=fallback-x11
# Wayland access
- --socket=wayland
# Network access (for downloading videos & updates)
- --share=network
# GPU acceleration (for hardware-accelerated encoding)
- --device=dri
- # File system access for saving downloads
- - --filesystem=home
+ # File system access for saving downloads (least privilege; arbitrary
+ # user-chosen locations go through the file-chooser portal, not --filesystem=home)
- --filesystem=xdg-videos
- --filesystem=xdg-download
# Notifications
diff --git a/electron-builder.base.yml b/electron-builder.base.yml
index fb673f7..486aecf 100644
--- a/electron-builder.base.yml
+++ b/electron-builder.base.yml
@@ -1,6 +1,15 @@
appId: com.burnttoasters.rosi
productName: Rosi
asar: true
+# Harden the packaged Electron binary. electron-builder flips these via
+# @electron/fuses during packaging and re-signs the binary afterwards.
+electronFuses:
+ runAsNode: false
+ enableCookieEncryption: true
+ enableNodeOptionsEnvironmentVariable: false
+ enableNodeCliInspectArguments: false
+ enableEmbeddedAsarIntegrityValidation: true
+ onlyLoadAppFromAsar: true
files:
- 'dist/**/*'
- 'src/renderer/**/*'
@@ -23,6 +32,7 @@ win:
to: assets/
filter:
- 'yt-dlp*.exe'
+ - 'YT-DLP-NOTICES.txt'
- from: resources/ffmpeg/win/${arch}
to: ffmpeg
filter:
@@ -48,6 +58,8 @@ mac:
extraResources:
- from: assets/yt-dlp_macos
to: assets/yt-dlp_macos
+ - from: assets/YT-DLP-NOTICES.txt
+ to: assets/YT-DLP-NOTICES.txt
- from: resources/ffmpeg/mac/${arch}
to: ffmpeg
filter:
@@ -72,6 +84,7 @@ linux:
to: assets/
filter:
- 'yt-dlp_linux*'
+ - 'YT-DLP-NOTICES.txt'
- from: resources/ffmpeg/linux/${arch}
to: ffmpeg
filter:
diff --git a/electron-builder.github.yml b/electron-builder.github.yml
index a8d33c2..035b8c3 100644
--- a/electron-builder.github.yml
+++ b/electron-builder.github.yml
@@ -12,4 +12,7 @@ mac:
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
notarize: true
-afterSign: build-scripts/sign-mac-helpers.js
+# Sign the bundled helper binaries (yt-dlp, ffmpeg, ffprobe) during afterPack —
+# BEFORE electron-builder signs/seals the .app — so the parent signature stays
+# valid and notarization is not invalidated by re-signing nested code.
+afterPack: build-scripts/sign-mac-helpers.js
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 192e432..1feb0de 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,5 +1,6 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
+import nounsanitized from 'eslint-plugin-no-unsanitized';
export default tseslint.config(
eslint.configs.recommended,
@@ -45,6 +46,25 @@ export default tseslint.config(
'no-empty': ['error', { allowEmptyCatch: true }],
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }],
+
+ // Type-aware unsafe-data-flow rules. Kept as warnings so they surface
+ // risky `any` flows without breaking the build; promote to 'error' as the
+ // remaining warnings are burned down.
+ '@typescript-eslint/no-unsafe-assignment': 'warn',
+ '@typescript-eslint/no-unsafe-member-access': 'warn',
+ '@typescript-eslint/no-unsafe-call': 'warn',
+ '@typescript-eslint/no-unsafe-return': 'warn',
+ '@typescript-eslint/no-unsafe-argument': 'warn',
+ },
+ },
+ {
+ // Flag unsanitized DOM sinks (innerHTML/insertAdjacentHTML/etc.) in the
+ // renderer, where untrusted yt-dlp/queue data is rendered.
+ files: ['src/renderer/**/*.ts'],
+ plugins: { 'no-unsanitized': nounsanitized },
+ rules: {
+ 'no-unsanitized/method': 'error',
+ 'no-unsanitized/property': 'error',
},
},
{
diff --git a/package-lock.json b/package-lock.json
index 3591efd..89ab41b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "rosi",
- "version": "4.1.3-beta.1",
+ "version": "4.1.3-beta.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rosi",
- "version": "4.1.3-beta.1",
+ "version": "4.1.3-beta.2",
"license": "MPL-2.0",
"dependencies": {
"electron-log": "^5.3.4",
@@ -22,6 +22,7 @@
"electron": "^42.0.0",
"electron-builder": "^26.7.0",
"eslint": "^10.3.0",
+ "eslint-plugin-no-unsanitized": "^4.1.2",
"husky": "^9.1.7",
"js-yaml": "^4.1.0",
"jsdom": "^29.1.1",
@@ -32,8 +33,8 @@
"vitest": "^4.0.16"
},
"engines": {
- "node": ">=24.x",
- "npm": ">=10.x"
+ "node": ">=24",
+ "npm": ">=11"
}
},
"node_modules/@asamuzakjp/css-color": {
@@ -205,9 +206,9 @@
}
},
"node_modules/@csstools/css-color-parser": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz",
- "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.4.tgz",
+ "integrity": "sha512-yI8kNhHiOrLb8Rlulsk07DeQz0PwyT69FX9dkz5rAp7p9RUwFKEXnZpBGzURiLHgi66YqIWxOHn1nij8Lrg27Q==",
"dev": true,
"funding": [
{
@@ -301,9 +302,9 @@
}
},
"node_modules/@electron-internal/extract-zip": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.2.tgz",
- "integrity": "sha512-VJuNETNPEhrmQEZezeTZO5TZMV+dobBRyJ7zHjGJWIhMS7m7W1UeClt69u4hkUxv9ZZVxuli/E9Yvc4gDNHGsg==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.3.tgz",
+ "integrity": "sha512-OjKpjB7gohtEjZiq6nDx1egqjZJhGPN1iFOIED+NFhB/MMkXw/XRcHjh1DGXKT5z2W9eW7Jy2UKU3gpjvusFTQ==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
@@ -996,14 +997,14 @@
}
},
"node_modules/@peculiar/asn1-schema": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz",
- "integrity": "sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==",
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz",
+ "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@peculiar/utils": "^2.0.2",
- "asn1js": "^3.0.6",
+ "asn1js": "^3.0.10",
"tslib": "^2.8.1"
}
},
@@ -1470,9 +1471,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.9.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz",
- "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==",
+ "version": "25.9.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz",
+ "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1884,9 +1885,9 @@
}
},
"node_modules/acorn": {
- "version": "8.16.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
- "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -2228,9 +2229,9 @@
}
},
"node_modules/ast-v8-to-istanbul": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.3.tgz",
- "integrity": "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz",
+ "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3244,9 +3245,9 @@
}
},
"node_modules/electron/node_modules/@types/node": {
- "version": "24.13.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.1.tgz",
- "integrity": "sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg==",
+ "version": "24.13.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
+ "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3411,11 +3412,14 @@
}
},
"node_modules/eslint": {
- "version": "10.4.1",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz",
- "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==",
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz",
+ "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==",
"dev": true,
"license": "MIT",
+ "workspaces": [
+ "packages/*"
+ ],
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2",
@@ -3466,6 +3470,16 @@
}
}
},
+ "node_modules/eslint-plugin-no-unsanitized": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.1.5.tgz",
+ "integrity": "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
"node_modules/eslint-scope": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
@@ -3768,17 +3782,17 @@
"license": "ISC"
},
"node_modules/form-data": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
- "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.2",
- "mime-types": "^2.1.12"
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
},
"engines": {
"node": ">= 6"
@@ -5395,9 +5409,9 @@
}
},
"node_modules/obug": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz",
- "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
+ "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
diff --git a/package.json b/package.json
index 6ce0448..8f19e24 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "rosi",
- "version": "4.1.3-beta.1",
+ "version": "4.1.3-beta.2",
"private": true,
"description": "Electron GUI for yt-dlp",
"desktopName": "com.burnttoasters.rosi.desktop",
@@ -13,8 +13,8 @@
"main": "dist/main/main.js",
"packageManager": "npm@11.10.1",
"engines": {
- "node": ">=24.x",
- "npm": ">=10.x"
+ "node": ">=24",
+ "npm": ">=11"
},
"scripts": {
"gitprune": "node build-scripts/git-prune-local-branches.js",
@@ -24,6 +24,8 @@
"hooks:install": "node build-scripts/install-hooks.js",
"clean": "node build-scripts/dist-tools.js clean && node build-scripts/dist-tools.js clean-release",
"licenses": "npx npm-license-crawler --production --json licenses.json",
+ "ytdlp:check": "node build-scripts/check-ytdlp.js",
+ "ytdlp:check:generate": "node build-scripts/check-ytdlp.js --generate",
"compile:main": "tsc --project tsconfig.main.json",
"compile:renderer": "tsc --project tsconfig.renderer.json",
"compile": "node build-scripts/dist-tools.js clean && npm run compile:main && npm run compile:renderer && node build-scripts/dist-tools.js copy",
@@ -38,7 +40,7 @@
"ffmpeg:check:linux:x64": "npm run ffmpeg:check -- --target linux:x64",
"ffmpeg:check:linux:arm64": "npm run ffmpeg:check -- --target linux:arm64",
"prerelease:prepare": "node build-scripts/release-warning.js",
- "prebuild:base": "npm run compile && npm run licenses",
+ "prebuild:base": "npm run compile && npm run licenses && npm run ytdlp:check",
"prebuild": "npm run prebuild:base && npm run ffmpeg:check:current",
"prebuild:win": "npm run prebuild:base && npm run ffmpeg:check:win",
"prebuild:win:x64": "npm run prebuild:base && npm run ffmpeg:check:win:x64",
@@ -61,8 +63,8 @@
"test:watch": "vitest",
"test:all": "node build-scripts/test-all.js",
"test:cov": "vitest run --coverage && node build-scripts/check-coverage-thresholds.js",
- "lint": "eslint src/",
- "lint:fix": "eslint src/ --fix",
+ "lint": "eslint src/ build-scripts/",
+ "lint:fix": "eslint src/ build-scripts/ --fix",
"format": "prettier --write src/",
"format:check": "prettier --check src/",
"start": "npm run clean && npm run compile && electron .",
@@ -119,7 +121,7 @@
"setup:deb": "sudo apt update && sudo apt install -y build-essential libgtk-3-dev libnotify-dev libnss3-dev libxss-dev libxtst-dev libatspi2.0-dev uuid-dev libsecret-1-dev libx11-dev rpm flatpak flatpak-builder && sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo"
},
"overrides": {
- "tar": "^7.5.3"
+ "tar": "^7.5.8"
},
"repository": {
"type": "git",
@@ -140,6 +142,7 @@
"electron": "^42.0.0",
"electron-builder": "^26.7.0",
"eslint": "^10.3.0",
+ "eslint-plugin-no-unsanitized": "^4.1.2",
"husky": "^9.1.7",
"js-yaml": "^4.1.0",
"jsdom": "^29.1.1",
diff --git a/src/main/downloader.ts b/src/main/downloader.ts
index 8dee011..1bfe36e 100644
--- a/src/main/downloader.ts
+++ b/src/main/downloader.ts
@@ -642,6 +642,7 @@ export function startDownload(
: resolvedDownloadDir;
const relativePath = path.relative(compareDownloadDir, compareFilePath);
if (
+ path.isAbsolute(relativePath) ||
relativePath === '..' ||
relativePath.startsWith(`..${path.sep}`) ||
relativePath.startsWith('../')
diff --git a/src/main/platform.ts b/src/main/platform.ts
index 931bc37..96c36ad 100644
--- a/src/main/platform.ts
+++ b/src/main/platform.ts
@@ -393,7 +393,7 @@ function probeYtdlpBinary(ytdlpPath: string): Promise<{ ok: boolean; detail: str
let stderr = '';
let stdout = '';
const proc = spawn(ytdlpPath, ['--version'], {
- env: { ...process.env, PATH: buildEnhancedPath() },
+ env: { ...buildSafeEnv(), PATH: buildEnhancedPath() },
shell: false,
});
diff --git a/src/renderer/css/01-base.css b/src/renderer/css/01-base.css
index 97da7f4..6771d59 100644
--- a/src/renderer/css/01-base.css
+++ b/src/renderer/css/01-base.css
@@ -45,7 +45,7 @@
--font-sans: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'IBM Plex Mono', 'SF Mono', 'Fira Code', 'Consolas', monospace;
- --text-xs: 0.6rem;
+ --text-xs: 0.72rem;
--text-sm: 0.75rem;
--text-base: 0.92rem;
--text-md: 1rem;
@@ -72,7 +72,7 @@
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.8);
--text-tertiary: rgba(255, 255, 255, 0.7);
- --text-muted: rgba(255, 255, 255, 0.45);
+ --text-muted: rgba(255, 255, 255, 0.62);
--accent: #22d3ee;
--accent-light: #67e8f9;
@@ -194,7 +194,7 @@
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.78);
--text-tertiary: rgba(255, 255, 255, 0.7);
- --text-muted: rgba(255, 255, 255, 0.45);
+ --text-muted: rgba(255, 255, 255, 0.62);
--accent: #8b5cf6;
--accent-light: #a78bfa;
--accent-dark: #7c3aed;
@@ -237,7 +237,7 @@
--text-primary: #0f172a;
--text-secondary: rgba(15, 23, 42, 0.82);
--text-tertiary: rgba(15, 23, 42, 0.7);
- --text-muted: rgba(15, 23, 42, 0.52);
+ --text-muted: rgba(15, 23, 42, 0.66);
--accent: #2563eb;
--accent-light: #3b82f6;
--accent-dark: #1d4ed8;
diff --git a/src/renderer/fonts/OFL.txt b/src/renderer/fonts/OFL.txt
new file mode 100644
index 0000000..1e479dc
--- /dev/null
+++ b/src/renderer/fonts/OFL.txt
@@ -0,0 +1,97 @@
+ROSI bundles the following fonts, both licensed under the SIL Open Font
+License, Version 1.1:
+
+ Manrope
+ Copyright 2018 The Manrope Project Authors
+ (https://github.com/sharanda/manrope)
+
+ IBM Plex Mono
+ Copyright (c) 2017 IBM Corp. with Reserved Font Name "Plex"
+ (https://github.com/IBM/plex)
+
+The full license text follows.
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply to any
+document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may include
+source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical writer or
+other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components, in
+Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or in
+the appropriate machine-readable metadata fields within text or binary
+files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any Modified
+Version, except to acknowledge the contribution(s) of the Copyright
+Holder(s) and the Author(s) or with their explicit written permission.
+
+5) The Font Software, modified or unmodified, in part or in whole, must be
+distributed entirely under this license, and must not be distributed under
+any other license. The requirement for fonts to remain under this license
+does not apply to any document created using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are not
+met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
+COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
+DEALINGS IN THE FONT SOFTWARE.
diff --git a/src/renderer/index.html b/src/renderer/index.html
index 5264d02..d3e128a 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -7,23 +7,7 @@
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data: https:; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'self';"
/>
ROSI
-
+
diff --git a/src/renderer/licenses-iframe.html b/src/renderer/licenses-iframe.html
index 5f5a1e6..7747c0e 100644
--- a/src/renderer/licenses-iframe.html
+++ b/src/renderer/licenses-iframe.html
@@ -4,7 +4,7 @@
ROSI License & 3rd Party Licenses/Credits