diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml
index 83aa145..729b425 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 in shipped code)
+ run: npm audit --omit=dev --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 7827e96..440ec92 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,35 +1,31 @@
-
# ⬇️ Downloads
| Windows |
macOS |
Linux |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **EXE:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Windows-x64.exe) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Windows-arm64.exe) | **[Universal DMG](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-MacOS-universal.dmg)** | **AppImage:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Linux-x86_64.AppImage) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/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 898c94e..8f1b1f4 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,10 @@
-
+
+
+
+
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 2778ef0..6e80755 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "rosi",
- "version": "4.1.2",
+ "version": "4.1.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rosi",
- "version": "4.1.2",
+ "version": "4.1.3",
"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.7",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.7.tgz",
+ "integrity": "sha512-CmjJFQTFQx/U/xNJhSjCQ0ilpesPmNQ8+eOUeM/+kDOVW33qsIjeOXc27vrQDdWVkf83ZSWwtg7kXSUvKDJ8cQ==",
"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": {
@@ -954,14 +955,14 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
- "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
+ "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
- "@tybys/wasm-util": "^0.10.1"
+ "@tybys/wasm-util": "^0.10.2"
},
"funding": {
"type": "github",
@@ -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": {
@@ -1490,17 +1491,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.61.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz",
- "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==",
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz",
+ "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.61.0",
- "@typescript-eslint/type-utils": "8.61.0",
- "@typescript-eslint/utils": "8.61.0",
- "@typescript-eslint/visitor-keys": "8.61.0",
+ "@typescript-eslint/scope-manager": "8.61.1",
+ "@typescript-eslint/type-utils": "8.61.1",
+ "@typescript-eslint/utils": "8.61.1",
+ "@typescript-eslint/visitor-keys": "8.61.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -1513,7 +1514,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.61.0",
+ "@typescript-eslint/parser": "^8.61.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
@@ -1529,16 +1530,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.61.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz",
- "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==",
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz",
+ "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.61.0",
- "@typescript-eslint/types": "8.61.0",
- "@typescript-eslint/typescript-estree": "8.61.0",
- "@typescript-eslint/visitor-keys": "8.61.0",
+ "@typescript-eslint/scope-manager": "8.61.1",
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/typescript-estree": "8.61.1",
+ "@typescript-eslint/visitor-keys": "8.61.1",
"debug": "^4.4.3"
},
"engines": {
@@ -1554,14 +1555,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.61.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz",
- "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==",
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz",
+ "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.61.0",
- "@typescript-eslint/types": "^8.61.0",
+ "@typescript-eslint/tsconfig-utils": "^8.61.1",
+ "@typescript-eslint/types": "^8.61.1",
"debug": "^4.4.3"
},
"engines": {
@@ -1576,14 +1577,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.61.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz",
- "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==",
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz",
+ "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.61.0",
- "@typescript-eslint/visitor-keys": "8.61.0"
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/visitor-keys": "8.61.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1594,9 +1595,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.61.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz",
- "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==",
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz",
+ "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1611,15 +1612,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.61.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz",
- "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==",
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz",
+ "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.61.0",
- "@typescript-eslint/typescript-estree": "8.61.0",
- "@typescript-eslint/utils": "8.61.0",
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/typescript-estree": "8.61.1",
+ "@typescript-eslint/utils": "8.61.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1636,9 +1637,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.61.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz",
- "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==",
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz",
+ "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1650,16 +1651,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.61.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz",
- "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==",
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz",
+ "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.61.0",
- "@typescript-eslint/tsconfig-utils": "8.61.0",
- "@typescript-eslint/types": "8.61.0",
- "@typescript-eslint/visitor-keys": "8.61.0",
+ "@typescript-eslint/project-service": "8.61.1",
+ "@typescript-eslint/tsconfig-utils": "8.61.1",
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/visitor-keys": "8.61.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1678,16 +1679,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.61.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz",
- "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==",
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz",
+ "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.61.0",
- "@typescript-eslint/types": "8.61.0",
- "@typescript-eslint/typescript-estree": "8.61.0"
+ "@typescript-eslint/scope-manager": "8.61.1",
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/typescript-estree": "8.61.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1702,13 +1703,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.61.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz",
- "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==",
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz",
+ "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.61.0",
+ "@typescript-eslint/types": "8.61.1",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1720,14 +1721,14 @@
}
},
"node_modules/@vitest/coverage-v8": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz",
- "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz",
+ "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
- "@vitest/utils": "4.1.8",
+ "@vitest/utils": "4.1.9",
"ast-v8-to-istanbul": "^1.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
@@ -1741,8 +1742,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "@vitest/browser": "4.1.8",
- "vitest": "4.1.8"
+ "@vitest/browser": "4.1.9",
+ "vitest": "4.1.9"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -1751,16 +1752,16 @@
}
},
"node_modules/@vitest/expect": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz",
- "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
+ "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.8",
- "@vitest/utils": "4.1.8",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@@ -1769,13 +1770,13 @@
}
},
"node_modules/@vitest/mocker": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz",
- "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz",
+ "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.1.8",
+ "@vitest/spy": "4.1.9",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -1796,9 +1797,9 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz",
- "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
+ "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1809,13 +1810,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz",
- "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz",
+ "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.1.8",
+ "@vitest/utils": "4.1.9",
"pathe": "^2.0.3"
},
"funding": {
@@ -1823,14 +1824,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz",
- "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz",
+ "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.8",
- "@vitest/utils": "4.1.8",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/utils": "4.1.9",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -1839,9 +1840,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz",
- "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz",
+ "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -1849,13 +1850,13 @@
}
},
"node_modules/@vitest/utils": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz",
- "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz",
+ "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.8",
+ "@vitest/pretty-format": "4.1.9",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@@ -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": {
@@ -3071,9 +3072,9 @@
}
},
"node_modules/electron": {
- "version": "42.4.0",
- "resolved": "https://registry.npmjs.org/electron/-/electron-42.4.0.tgz",
- "integrity": "sha512-OXXqh9LD9KxXPv2Fe25EfU9N9AvWTuV6V81sfhQaNvTAXCd9ONA+Q4OWvMe+CmYD6xIwjFxGGtG/ZphDYYC5OQ==",
+ "version": "42.4.1",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-42.4.1.tgz",
+ "integrity": "sha512-8CYHJP5O4wFO+ycoJR98yy907MmPeo+vWXrzjxmGGgRNKqv8pOjjm+wphO0CCgQJnBU7+QUPSJS4QXhbKrO50w==",
"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"
@@ -5322,9 +5336,9 @@
}
},
"node_modules/node-gyp/node_modules/undici": {
- "version": "6.26.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz",
- "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==",
+ "version": "6.27.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
+ "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -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",
@@ -6448,22 +6462,22 @@
}
},
"node_modules/tldts": {
- "version": "7.4.2",
- "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz",
- "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==",
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.3.tgz",
+ "integrity": "sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tldts-core": "^7.4.2"
+ "tldts-core": "^7.4.3"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
- "version": "7.4.2",
- "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz",
- "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==",
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.3.tgz",
+ "integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==",
"dev": true,
"license": "MIT"
},
@@ -6584,16 +6598,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.61.0",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz",
- "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==",
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz",
+ "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.61.0",
- "@typescript-eslint/parser": "8.61.0",
- "@typescript-eslint/typescript-estree": "8.61.0",
- "@typescript-eslint/utils": "8.61.0"
+ "@typescript-eslint/eslint-plugin": "8.61.1",
+ "@typescript-eslint/parser": "8.61.1",
+ "@typescript-eslint/typescript-estree": "8.61.1",
+ "@typescript-eslint/utils": "8.61.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -6608,9 +6622,9 @@
}
},
"node_modules/undici": {
- "version": "7.27.2",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz",
- "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -6764,19 +6778,19 @@
}
},
"node_modules/vitest": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz",
- "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz",
+ "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/expect": "4.1.8",
- "@vitest/mocker": "4.1.8",
- "@vitest/pretty-format": "4.1.8",
- "@vitest/runner": "4.1.8",
- "@vitest/snapshot": "4.1.8",
- "@vitest/spy": "4.1.8",
- "@vitest/utils": "4.1.8",
+ "@vitest/expect": "4.1.9",
+ "@vitest/mocker": "4.1.9",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/runner": "4.1.9",
+ "@vitest/snapshot": "4.1.9",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@@ -6804,12 +6818,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.8",
- "@vitest/browser-preview": "4.1.8",
- "@vitest/browser-webdriverio": "4.1.8",
- "@vitest/coverage-istanbul": "4.1.8",
- "@vitest/coverage-v8": "4.1.8",
- "@vitest/ui": "4.1.8",
+ "@vitest/browser-playwright": "4.1.9",
+ "@vitest/browser-preview": "4.1.9",
+ "@vitest/browser-webdriverio": "4.1.9",
+ "@vitest/coverage-istanbul": "4.1.9",
+ "@vitest/coverage-v8": "4.1.9",
+ "@vitest/ui": "4.1.9",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
diff --git a/package.json b/package.json
index 93cd983..47145ec 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "rosi",
- "version": "4.1.2",
+ "version": "4.1.3",
"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/deno.ts b/src/main/deno.ts
index 894d983..33e85c4 100644
--- a/src/main/deno.ts
+++ b/src/main/deno.ts
@@ -120,11 +120,11 @@ export async function installDeno(
resolve({ success: false, error: 'Installation timed out after 2 minutes' });
}, DENO_INSTALL_TIMEOUT_MS);
- proc.stdout?.on('data', (data) => {
+ proc.stdout?.on('data', (data: Buffer) => {
if (output.length < MAX_OUTPUT_BUFFER) output += data.toString();
});
- proc.stderr?.on('data', (data) => {
+ proc.stderr?.on('data', (data: Buffer) => {
if (error.length < MAX_ERROR_BUFFER) error += data.toString();
});
diff --git a/src/main/download/commandBuilders.ts b/src/main/download/commandBuilders.ts
index 0e94097..cbe6088 100644
--- a/src/main/download/commandBuilders.ts
+++ b/src/main/download/commandBuilders.ts
@@ -55,7 +55,7 @@ export function probeMediaCodecs(ffmpegCommand: string, inputPath: string): Prom
return;
}
- proc.stderr?.on('data', (data) => {
+ proc.stderr?.on('data', (data: Buffer) => {
if (stderr.length < MAX_ERROR_BUFFER) stderr += data.toString();
});
proc.on('close', () => finish(parse()));
diff --git a/src/main/download/videoInfo.ts b/src/main/download/videoInfo.ts
index f0facb1..793a214 100644
--- a/src/main/download/videoInfo.ts
+++ b/src/main/download/videoInfo.ts
@@ -31,17 +31,19 @@ export function parseVideoInfo(jsonString: string): VideoInfo | null {
const data = parsed as Record;
const isPlaylist = data._type === 'playlist' || Array.isArray(data.entries);
- const entries = Array.isArray(data.entries) ? data.entries : null;
+ const entries: unknown[] | null = Array.isArray(data.entries)
+ ? (data.entries as unknown[])
+ : null;
let thumbnail = pickThumbnail(data.thumbnail);
if (!thumbnail && Array.isArray(data.thumbnails) && data.thumbnails.length > 0) {
- const last = data.thumbnails[data.thumbnails.length - 1];
+ const last: unknown = (data.thumbnails as unknown[])[data.thumbnails.length - 1];
if (last && typeof last === 'object') {
thumbnail = pickThumbnail((last as Record).url);
}
}
if (!thumbnail && entries && entries.length > 0) {
- const first = entries[0];
+ const first: unknown = entries[0];
if (first && typeof first === 'object') {
thumbnail = pickThumbnail((first as Record).thumbnail);
}
diff --git a/src/main/downloader.ts b/src/main/downloader.ts
index fb33807..f356ad5 100644
--- a/src/main/downloader.ts
+++ b/src/main/downloader.ts
@@ -3,7 +3,13 @@ import * as fs from 'fs';
import sanitize from 'sanitize-filename';
import { dialog } from 'electron';
import log from 'electron-log/main.js';
-import { spawnWithEnv, getEffectiveFfmpegPath, ytdlpBinary, isWindows } from './platform';
+import {
+ spawnWithEnv,
+ getEffectiveFfmpegPath,
+ resolveFfmpegLocationForYtdlp,
+ ytdlpBinary,
+ isWindows,
+} from './platform';
import { killChildProcess } from './processKill';
import { loadSettings, recordDownload } from './settings';
import {
@@ -330,7 +336,10 @@ async function runConversion(
sendProgress(session, `🖥️ Using GPU acceleration (${videoEncoder})`);
}
- const ffProc = spawnWithEnv(ffmpegCommand, ffmpegArgs);
+ const ffProc = spawnWithEnv(ffmpegCommand, ffmpegArgs, {
+ // New process group so kill(-pid) reaches any ffmpeg sub-processes too.
+ detached: !isWindows,
+ });
session.ffmpegProcess = ffProc;
const conversionTimeout = setTimeout(() => {
@@ -340,11 +349,11 @@ async function runConversion(
completeSession(session, '❌ Conversion failed (timeout).', 'failed');
}, FFMPEG_CONVERT_TIMEOUT_MS);
- ffProc.stdout?.on('data', (data) => {
+ ffProc.stdout?.on('data', (data: Buffer) => {
if (!isActiveSession(session)) return;
sendProgress(session, `[ffmpeg] ${data.toString().trim()}`);
});
- ffProc.stderr?.on('data', (data) => {
+ ffProc.stderr?.on('data', (data: Buffer) => {
if (!isActiveSession(session)) return;
sendProgress(session, `[ffmpeg] ${data.toString().trim()}`);
});
@@ -471,7 +480,7 @@ export function startDownload(
const settings = loadSettings();
const effectiveSettings: Settings = { ...settings };
const ffmpegCommand = getEffectiveFfmpegPath(options.ffmpegPath || settings.ffmpegPath);
- const ffmpegLocation = ffmpegCommand !== 'ffmpeg' ? path.dirname(ffmpegCommand) : null;
+ const ffmpegLocation = resolveFfmpegLocationForYtdlp(options.ffmpegPath || settings.ffmpegPath);
if (options.convertFormat !== undefined) {
if (typeof options.convertFormat === 'string' && options.convertFormat.trim() !== '') {
@@ -542,13 +551,16 @@ export function startDownload(
sendProgress(session, ` Command: ${ytdlpBinary} ${ytdlpArgs.join(' ')}`);
const ytProc = spawnWithEnv(ytdlpPath, ytdlpArgs, {
env: { PYTHONUNBUFFERED: '1' },
+ // On Unix, spawn as a new process-group leader so kill(-pid) delivers
+ // signals to yt-dlp AND any ffmpeg it spawns internally for merging.
+ detached: !isWindows,
});
session.ytdlpProcess = ytProc;
let downloadOutputData = '';
let downloadErrorData = '';
- ytProc.stdout?.on('data', (data) => {
+ ytProc.stdout?.on('data', (data: Buffer) => {
if (!isActiveSession(session)) return;
const message = data.toString();
if (downloadOutputData.length + message.length > MAX_OUTPUT_BUFFER) {
@@ -558,7 +570,7 @@ export function startDownload(
sendProgress(session, message.trim());
});
- ytProc.stderr?.on('data', (data) => {
+ ytProc.stderr?.on('data', (data: Buffer) => {
if (!isActiveSession(session)) return;
const message = data.toString();
if (downloadErrorData.length < MAX_ERROR_BUFFER) {
@@ -636,6 +648,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/main.ts b/src/main/main.ts
index dc622e0..849ebee 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -266,7 +266,7 @@ async function runRendererSmokeChecks(windowRef: BrowserWindow): Promise {
- if (output.length < 512) output += data.toString();
- });
- proc.on('close', (code: number | null) => {
- if (code === 0 && output) {
- const firstLine = output.split('\n')[0]?.trim() ?? '';
- log.info(`Bundled ffmpeg verified: ${firstLine}`);
- } else {
- log.warn(`Bundled ffmpeg at ${ffmpegPath} exited with code ${code}`);
- }
- });
- proc.on('error', (err: Error) => {
- log.warn(`Bundled ffmpeg at ${ffmpegPath} failed to execute: ${err.message}`);
- });
- } catch (err) {
- log.warn(`Failed to verify bundled ffmpeg: ${(err as Error).message}`);
+ ensureFfmpegHelperBinaries(bundledDir);
+
+ const ext = isWindows ? '.exe' : '';
+ const helpers = [
+ { label: 'ffmpeg', filePath: path.join(bundledDir, `ffmpeg${ext}`) },
+ { label: 'ffprobe', filePath: path.join(bundledDir, `ffprobe${ext}`) },
+ ];
+
+ for (const helper of helpers) {
+ if (!fs.existsSync(helper.filePath)) {
+ log.warn(`Bundled ${helper.label} not found at ${helper.filePath}`);
+ continue;
+ }
+
+ try {
+ const proc = spawnWithEnv(helper.filePath, ['-version'], { shell: false });
+ let output = '';
+ proc.stdout?.on('data', (data: Buffer) => {
+ if (output.length < 512) output += data.toString();
+ });
+ proc.on('close', (code: number | null) => {
+ if (code === 0 && output) {
+ const firstLine = output.split('\n')[0]?.trim() ?? '';
+ log.info(`Bundled ${helper.label} verified: ${firstLine}`);
+ } else {
+ log.warn(`Bundled ${helper.label} at ${helper.filePath} exited with code ${code}`);
+ }
+ });
+ proc.on('error', (err: Error) => {
+ log.warn(`Bundled ${helper.label} at ${helper.filePath} failed to execute: ${err.message}`);
+ });
+ } catch (err) {
+ log.warn(`Failed to verify bundled ${helper.label}: ${(err as Error).message}`);
+ }
}
}
@@ -331,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/main/processKill.ts b/src/main/processKill.ts
index 8e2ca7c..e1a7a7f 100644
--- a/src/main/processKill.ts
+++ b/src/main/processKill.ts
@@ -7,10 +7,33 @@ export function killChildProcess(proc: ChildProcess | null, label: string) {
if (!proc || proc.killed) return;
const pid = proc.pid;
try {
- proc.kill('SIGTERM');
+ if (!isWindows && pid) {
+ // Try group kill first. If the process was spawned with detached:true it is
+ // a process-group leader (PGID = PID), so kill(-pid) delivers the signal to
+ // it AND every descendant (e.g. yt-dlp's internal merge-ffmpeg). If it was
+ // NOT spawned detached its PGID ≠ pid, so kill(-pid) targets an empty group
+ // and throws ESRCH harmlessly — we fall back to a direct kill.
+ try {
+ process.kill(-pid, 'SIGTERM');
+ } catch {
+ proc.kill('SIGTERM');
+ }
+ } else {
+ proc.kill('SIGTERM');
+ }
const forceKillTimer = setTimeout(() => {
try {
- if (!proc.killed) proc.kill('SIGKILL');
+ if (!proc.killed) {
+ if (!isWindows && pid) {
+ try {
+ process.kill(-pid, 'SIGKILL');
+ } catch {
+ proc.kill('SIGKILL');
+ }
+ } else {
+ proc.kill('SIGKILL');
+ }
+ }
} catch {}
if (isWindows && pid) {
execFile('taskkill', ['/PID', String(pid), '/T', '/F'], () => {});
diff --git a/src/main/settings.ts b/src/main/settings.ts
index cc6012a..9186956 100644
--- a/src/main/settings.ts
+++ b/src/main/settings.ts
@@ -220,7 +220,7 @@ export function loadSettings(): Settings {
return { ...defaultSettings };
}
const raw = fs.readFileSync(settingsPath, 'utf-8');
- const loaded = JSON.parse(raw);
+ const loaded: unknown = JSON.parse(raw);
return normalizeSettingsVersion(migrateSettings(loaded));
} catch (error) {
log.warn('Failed to load settings, using defaults:', error);
@@ -279,8 +279,9 @@ export function loadStats(): DownloadStats {
try {
if (!fs.existsSync(statsPath)) return getDefaultStats();
const raw = fs.readFileSync(statsPath, 'utf-8');
- const loaded = JSON.parse(raw);
- if (!loaded || typeof loaded !== 'object' || Array.isArray(loaded)) return getDefaultStats();
+ const parsed: unknown = JSON.parse(raw);
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return getDefaultStats();
+ const loaded = parsed as Record;
const stats = getDefaultStats();
if (typeof loaded.totalDownloads === 'number') stats.totalDownloads = loaded.totalDownloads;
if (typeof loaded.successfulDownloads === 'number')
@@ -296,7 +297,7 @@ export function loadStats(): DownloadStats {
!Array.isArray(loaded.formatCounts) &&
loaded.formatCounts !== null
)
- stats.formatCounts = { ...loaded.formatCounts };
+ stats.formatCounts = { ...(loaded.formatCounts as Record) };
if (typeof loaded.firstDownloadAt === 'number') stats.firstDownloadAt = loaded.firstDownloadAt;
if (typeof loaded.lastDownloadAt === 'number') stats.lastDownloadAt = loaded.lastDownloadAt;
return stats;
@@ -391,7 +392,7 @@ export async function importSettingsFromFile(
return false;
}
const raw = fs.readFileSync(filePaths[0], 'utf-8');
- const loaded = JSON.parse(raw);
+ const loaded: unknown = JSON.parse(raw);
if (!loaded || typeof loaded !== 'object' || Array.isArray(loaded)) {
log.warn('Imported settings file has invalid structure.');
return 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/css/02-sidebar.css b/src/renderer/css/02-sidebar.css
index 9aa3516..c10d549 100644
--- a/src/renderer/css/02-sidebar.css
+++ b/src/renderer/css/02-sidebar.css
@@ -170,6 +170,12 @@
pointer-events: none;
}
+/* Show a visible focus ring on the slider proxy when the hidden input is keyboard-focused. */
+.toggle-switch input:focus-visible + .toggle-slider {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+}
+
.toggle-slider {
position: relative;
width: var(--toggle-width);
diff --git a/src/renderer/css/08-wizard.css b/src/renderer/css/08-wizard.css
index 80843ce..9415c64 100644
--- a/src/renderer/css/08-wizard.css
+++ b/src/renderer/css/08-wizard.css
@@ -65,6 +65,12 @@
background: var(--accent-subtle);
}
+/* Focus ring on the visible card when its hidden radio is keyboard-focused. */
+.wizard-theme-option input:focus-visible + .wizard-theme-card {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+}
+
.wizard-theme-card:hover {
border-color: var(--border-default);
}
@@ -131,6 +137,12 @@
pointer-events: none;
}
+/* Focus ring on wizard toggle slider proxy. */
+.wizard-toggle input:focus-visible + .toggle-slider {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+}
+
.wizard-toggle input:checked + .toggle-slider {
background: var(--accent);
}
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..9ad0807 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
-
+
@@ -315,9 +299,14 @@ Browser Integration
Browser: