Skip to content

feat(security): IPC hardening, trusted-access contract & app-media protocol#359

Open
verivusOSS-releases wants to merge 6 commits intosiddharthvaddem:mainfrom
verivusOSS-releases:security/ipc-hardening
Open

feat(security): IPC hardening, trusted-access contract & app-media protocol#359
verivusOSS-releases wants to merge 6 commits intosiddharthvaddem:mainfrom
verivusOSS-releases:security/ipc-hardening

Conversation

@verivusOSS-releases
Copy link
Copy Markdown

@verivusOSS-releases verivusOSS-releases commented Apr 6, 2026

Description

Harden Electron IPC file-read handlers with a trusted-access contract, replace permissive path checks with strict isTrustedMediaPath validation, introduce the app-media:// custom protocol to restore webSecurity: true, and add symlink resolution to prevent filesystem escapes.

Motivation

The existing IPC handlers allowed reading files from any directory once a path was normalized, making them vulnerable to path traversal and symlink-based attacks. With webSecurity: false on the editor window, the renderer could directly access local files. These changes establish a proper security boundary between renderer and main process.

Type of Change

  • New Feature
  • Bug Fix
  • Refactor / Code Cleanup
  • Documentation Update
  • Other (please specify)

Related Issue(s)

Related to #352 (path traversal in IPC file read handlers) — this PR provides a more comprehensive fix.

Screenshots / Video

N/A — security hardening with no UI changes.

Testing

  1. npm test — all 74 tests pass, including new regression tests
  2. Manual: open a .openscreen project referencing a file outside the recordings directory → should be rejected with "Access denied"
  3. Manual: verify video playback works through app-media:// protocol (record a video, play it back in editor)
  4. Manual: create a symlink inside recordings dir pointing outside → app-media:// should resolve it and reject

New test files:

  • src/lib/securityValidation.test.tsisTrustedMediaPath, sanitizeFileName, normalizeVideoSourcePath
  • src/hooks/useScreenRecorder.test.ts — recorder error lifecycle
  • src/components/video-editor/projectPersistence.test.ts — project persistence security
  • src/lib/compositeLayout.test.ts — vertical-stack layout bounds

Checklist

  • I have performed a self-review of my code.
  • I have added any necessary screenshots or videos.
  • I have linked related issue(s) and updated the changelog if applicable.

Methodology

These changes were developed using a multi-LLM review workflow orchestrated through llm-cli-gateway, an MCP-based tool for coordinating parallel code analysis across multiple AI models.

Process:

  1. Spec & planning — threat model and implementation plan developed collaboratively (spec, implementation plan)
  2. Implementation — Claude Code (Opus) implemented the security hardening based on the plan
  3. Cross-model review — OpenAI Codex independently reviewed the implementation, identifying issues with filename sanitization, symlink safety, path validation completeness, and error race conditions
  4. Fixes applied — review findings were addressed in dedicated fix commits (see fix(security): address Codex review findings commits)

This approach surfaces issues that a single-model workflow might miss, since each model brings different heuristics and blind spots to security analysis.


Thank you for contributing!

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling and recovery during screen recording with better cleanup
    • Enhanced validation of media file paths and external URLs for increased security
  • Tests

    • Added test coverage for project persistence validation and media handling
    • Added test coverage for screen recorder bitrate calculations and URL validation

sqry-release-plz bot and others added 5 commits April 6, 2026 04:18
… error lifecycle (U01, U02, U05)

U01: Add isTrustedMediaPath() and isAllowedExternalUrl() helpers to
centralize security validation for renderer-driven file and URL access.

U02: Apply trusted-access contract to read-binary-file (reject paths
outside recordings/project dirs), open-external-url (https/http only),
and reveal-in-folder (trusted paths only).

U05: Route recorder error handling through full teardown path — clear
media resources, sync main-process state, and stop webcam recorder
instead of only updating React state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… (U03)

Re-enable webSecurity on the editor BrowserWindow. Register a custom
app-media:// protocol that validates requested paths are under
RECORDINGS_DIR or the app assets directory before serving, returning
403 for untrusted paths. Update assetPath.ts to use app-media:// URLs
instead of raw file:// URLs in Electron builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… symlink safety, complete path validation, error race

1. Sanitize renderer-supplied filenames with path.basename() before
   writing to RECORDINGS_DIR (prevents traversal via ../../../).
2. Add isTrustedMediaPathRealpath() that resolves symlinks before
   checking trusted roots, used by read-binary-file.
3. Add isTrustedMediaPath checks to loadRecordedSessionForVideoPath,
   get-cursor-telemetry, and set-current-video-path handlers.
4. Set allowAutoFinalize.current = false in recorder error handler
   to prevent race with the stop listener's finalizeRecording call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve symlinks with fs.realpath() before serving files through the
app-media:// custom protocol. This prevents symlink-based escapes where
a symlink inside a trusted root points to a file outside it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…es (U07)

Add 16 new tests covering:
- URL scheme validation (https/http allow, file/javascript/data/blob reject)
- Bitrate computation with actual frame rate (threshold behavior, resolution tiers)
- Vertical-stack bounded layout with webcam present
- Project validation (invalid media, non-objects, missing version/editor)

Total: 37 tests across 8 files, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

The PR adds comprehensive security hardening for media file handling and external URL access. Changes include media path trust validation functions, a custom app-media:// protocol handler for serving recordings safely, stricter IPC validation for file reads/loads, external URL scheme restrictions (http/https only), and improved recorder error cleanup. The changes enforce path containment and restrict sensitive filesystem access across multiple IPC endpoints.

Changes

Cohort / File(s) Summary
Media Trust & IPC Security
electron/ipc/handlers.ts
Replaced path helper with sanitizeFileName() + direct path joining; added isTrustedMediaPath() and isTrustedMediaPathRealpath() trust gates; updated handlers (loadRecordedSessionForVideoPath, read-binary-file, get-cursor-telemetry, set-current-video-path, reveal-in-folder) to enforce trust validation; rewrote open-external-url to reject non-http/https schemes; added approveFilePath() call on exported video saves.
Protocol Registration & Media Serving
electron/main.ts, electron/windows.ts
Added registerMediaProtocol() import and initialization in app startup; implemented custom app-media:// protocol handler with path-containment validation (checks logical and symlink-resolved paths against RECORDINGS_DIR and assets root); removed webSecurity: false from editor window.
Asset Path & URL Validation
src/lib/assetPath.ts, src/lib/securityValidation.test.ts
Refactored asset URL construction to use app-media:// protocol with base pathname concatenation; added test suite for isAllowedExternalUrl() validating http/https schemes and rejecting malformed/dangerous URLs.
Recorder Error Handling & Bitrate Logic
src/hooks/useScreenRecorder.ts, src/hooks/useScreenRecorder.test.ts
Reworked error listener attachment to capture recorder instances and perform cleanup (disable auto-finalize, teardown media, reset state); added bitrate computation tests covering frame-rate boost logic (60+ fps) and pixel-count tiering (4K vs QHD).
Project & Media Validation
src/components/video-editor/projectPersistence.test.ts
Extended tests for validateProjectData and resolveProjectMedia to verify rejection of invalid inputs (null, non-objects, missing version/editor fields) and missing valid media.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

  • PR #352: Updates same IPC file-read and media-path authorization handlers (read-binary-file, get-cursor-telemetry, set-current-video-path) with path traversal prevention and approved media restrictions—direct code overlap.
  • PR #249: Modifies src/hooks/useScreenRecorder.ts with webcam device state and getUserMedia constraints—potential merge conflict in same module.

Poem

🔐 at 2am we lock the gates—
trust each path, reject the fakes,
app-media:// shines so bright,
files served safe through protocol's light ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main security hardening work: IPC hardening, trusted-access contract, and app-media protocol introduction.
Description check ✅ Passed The description covers all required template sections: clear motivation (security vulnerability fixes), type of change (New Feature + Bug Fix), related issue reference (#352), testing steps including new test files, and checklist completion.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2078088a3c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return { success: false, message: "Video path has not been approved" };
ipcMain.handle("set-current-video-path", async (_, videoPath: string) => {
const normalizedVideoPath = normalizeVideoSourcePath(videoPath);
if (!normalizedVideoPath || !isTrustedMediaPath(normalizedVideoPath)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor approved paths when setting current video

open-video-file-picker still uses approveReadableVideoPath(...) to allow user-selected files outside RECORDINGS_DIR, but this guard now rejects anything not under trusted roots. In the launch flow, selecting a valid video from Downloads/Desktop succeeds in the picker and then fails in setCurrentVideoPath, so importing external videos is effectively broken despite the approval mechanism remaining in place.

Useful? React with 👍 / 👎.

ipcMain.handle("reveal-in-folder", async (_, filePath: string) => {
try {
// shell.showItemInFolder doesn't return a value, it throws on error
if (!isTrustedMediaPath(filePath)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow reveal-in-folder for exported files

The export flow lets users save to arbitrary locations, and the UI calls revealInFolder(saveResult.path) afterward. This new trust-root check denies paths outside recordings/project directories, so the “Show in Folder” action now fails for common destinations like Downloads even though the path came from a user-approved save dialog.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@electron/ipc/handlers.ts`:
- Around line 234-240: sanitizeFileName currently only calls path.basename, so
embedded NULs and other unsafe characters can survive and the manifest is still
built from raw payload.screen.fileName; update sanitizeFileName to reject or
strip NULs (and other dangerous control chars) and return a canonical
safeScreenFileName, then use that safeScreenFileName everywhere (including where
the session manifest is assembled and any sibling paths under RECORDINGS_DIR) so
the manifest write and all blob writes derive from the same sanitized name and
cannot leave partial state behind; update references that use
payload.screen.fileName to instead use the returned safeScreenFileName.
- Around line 206-219: isTrustedMediaPath currently only trusts RECORDINGS_DIR
and the project dir, which breaks files explicitly approved by
approveReadableVideoPath/open-video-file-picker; update isTrustedMediaPath to
also consult the approvedPaths set (or collection used elsewhere) so that an
explicitly approved file path returns trusted. Specifically, modify the trust
check inside isTrustedMediaPath to return true if normalized matches or
startsWith any trustedRoots OR if normalized is present in approvedPaths (or
approvedPaths contains a canonical/normalized equivalent); ensure the same
change/logic is applied at the other occurrences called out (around the other
blocks that perform similar trust checks such as where read-binary-file and
set-current-video-path call it).
- Around line 275-277: The code currently checks
isTrustedMediaPath(normalizedVideoPath) then directly reads sidecar files like
`${normalizedVideoPath}.session.json` and `${normalizedVideoPath}.cursor.json`,
which can be symlinked outside trusted roots; change the flow so after
constructing the actual sidecar path you call fs.realpath (or equivalent) and
validate the resolved path with isTrustedMediaPathRealpath (the same function
used for read-binary-file) before calling read-binary-file (or reading the
file). Update both the handler that uses normalizedVideoPath and the other flow
around the same logic (the flow referenced near lines 599-601) to realpath each
sidecar file and re-run the realpath-based allow check (using
isTrustedMediaPathRealpath) prior to reading to ensure the final resolved file
is inside trusted roots.

In `@electron/main.ts`:
- Around line 343-345: Register the custom app-media scheme with privileged
permissions before the app is ready: add a top-level call to
protocol.registerSchemesAsPrivileged(...) specifying scheme 'app-media' with
privileges standard, secure, and supportFetchAPI (so fetch/URL handling works),
then leave the existing registerMediaProtocol() call inside app.whenReady().
Ensure the registerSchemesAsPrivileged invocation occurs before app.whenReady()
and references the same 'app-media' scheme used by registerMediaProtocol().

In `@electron/windows.ts`:
- Line 63: The current net.fetch call in the route handler drops incoming
request metadata and breaks Range requests; update the call that returns
net.fetch(pathToFileURL(realResolved).toString()) to forward the incoming
request's method, headers, and body (e.g., use request.method, request.headers,
request.body) so Range and other header-dependent behavior is preserved when
delegating to net.fetch; locate the usage in electron/windows.ts where
realResolved, request, and net.fetch are referenced and add those options to the
net.fetch invocation.

In `@src/hooks/useScreenRecorder.test.ts`:
- Around line 13-24: The test's computeBitrate helper accepts frameRate and
conditions on HIGH_FRAME_RATE_THRESHOLD, but the runtime in useScreenRecorder.ts
still uses TARGET_FRAME_RATE (so the boost is always applied); fix by making the
runtime computeBitrate behavior match the test: update useScreenRecorder's
bitrate calculation (the code around useScreenRecorder function lines that
reference TARGET_FRAME_RATE and HIGH_FRAME_RATE_BOOST) to accept or derive the
actual frameRate and apply HIGH_FRAME_RATE_BOOST only when frameRate >=
HIGH_FRAME_RATE_THRESHOLD, or alternatively change the test to call the real
helper from useScreenRecorder.ts so both use the same computeBitrate
implementation.

In `@src/hooks/useScreenRecorder.ts`:
- Around line 554-580: The error listener is attached too late—move the cleanup
handler to recorder creation so errors between recorder.start() and the current
listener don't bypass teardown; inside createRecorderHandle(), attach the same
"error" listener to activeScreenRecorder.recorder (the logic that sets
allowAutoFinalize.current = false, clears
screenRecorder.current/webcamRecorder.current, stops
activeWebcamRecorder.recorder if recording, calls teardownMedia(), calls
setRecording(false) and window.electronAPI?.setRecordingState(false), and clears
finalizingRecordingId.current when it matches activeRecordingId) before calling
recorder.start(), and also ensure any promise(s) like recordedBlobPromise that
may reject on error have their rejection explicitly consumed/handled in the
error path to avoid unhandled rejections.

In `@src/lib/compositeLayout.test.ts`:
- Around line 81-98: The test assumes the "vertical-stack" branch of
computeCompositeLayout honors maxContentSize but the current implementation
still uses canvasSize; update the vertical-stack branch in
computeCompositeLayout to constrain screenRect and webcamRect dimensions using
maxContentSize.width and maxContentSize.height (apply the same stacking/spacing
and rounding logic currently used for canvasSize), ensure total stacked height
(screenRect.height + webcamRect.height) is bounded by maxContentSize.height
(allowing the existing +1 rounding tolerance), and center the composed content
within the canvas so screenRect.x is > 0; keep existing behavior for other
presets unchanged.
- Around line 90-98: The test is too permissive by using
layout!.webcamRect?.height ?? 0; explicitly assert that the webcam rect exists
before measuring its height: add an assertion like
expect(layout!.webcamRect).not.toBeNull() (or truthy) and then use
layout!.webcamRect!.height in the subsequent constraint check (the code paths
using layout and webcamRect are the targets to update).

In `@src/lib/securityValidation.test.ts`:
- Around line 3-12: The test defines its own isAllowedExternalUrl which masks
regressions; extract the real isAllowedExternalUrl into a shared utility module
and have both the IPC handler and this test import that shared function instead
of redefining it. Move the current implementation (the URL parsing + protocol
check) into the new shared export, update the IPC handler to import and use that
export, and change src/lib/securityValidation.test.ts to import
isAllowedExternalUrl from the shared module so the test exercises the actual
production logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e7b0738f-d3b1-4688-8836-3563629c5983

📥 Commits

Reviewing files that changed from the base of the PR and between e571ecb and 2078088.

📒 Files selected for processing (9)
  • electron/ipc/handlers.ts
  • electron/main.ts
  • electron/windows.ts
  • src/components/video-editor/projectPersistence.test.ts
  • src/hooks/useScreenRecorder.test.ts
  • src/hooks/useScreenRecorder.ts
  • src/lib/assetPath.ts
  • src/lib/compositeLayout.test.ts
  • src/lib/securityValidation.test.ts

Comment on lines +206 to +219
function isTrustedMediaPath(filePath: string): boolean {
if (!filePath || typeof filePath !== "string") {
return false;
}
const normalized = normalizePath(filePath);

const trustedRoots: string[] = [normalizePath(RECORDINGS_DIR)];

if (currentProjectPath) {
trustedRoots.push(normalizePath(path.dirname(currentProjectPath)));
}

return trustedRoots.some((root) => normalized === root || normalized.startsWith(root + path.sep));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This drops the explicit-approval path entirely.

approveReadableVideoPath() / open-video-file-picker() still bless external videos, but isTrustedMediaPath() now only trusts RECORDINGS_DIR plus the current project directory. So a user-picked file from Downloads succeeds in the picker and then gets denied here by read-binary-file() / set-current-video-path(). If the new contract is “trusted roots + explicitly approved files,” approvedPaths still needs to participate in this check.

Also applies to: 548-553, 936-943

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 206 - 219, isTrustedMediaPath
currently only trusts RECORDINGS_DIR and the project dir, which breaks files
explicitly approved by approveReadableVideoPath/open-video-file-picker; update
isTrustedMediaPath to also consult the approvedPaths set (or collection used
elsewhere) so that an explicitly approved file path returns trusted.
Specifically, modify the trust check inside isTrustedMediaPath to return true if
normalized matches or startsWith any trustedRoots OR if normalized is present in
approvedPaths (or approvedPaths contains a canonical/normalized equivalent);
ensure the same change/logic is applied at the other occurrences called out
(around the other blocks that perform similar trust checks such as where
read-binary-file and set-current-video-path call it).

Comment on lines +234 to +240
function sanitizeFileName(fileName: string): string {
// Strip any path separators and parent-directory references to prevent traversal
const baseName = path.basename(fileName);
if (!baseName || baseName === "." || baseName === "..") {
throw new Error("Invalid file name");
}
return baseName;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The sanitized filename isn’t what the manifest write uses.

sanitizeFileName() currently just basename()s, so \0 survives, and the session manifest a few lines below is still derived from raw payload.screen.fileName. A crafted name can therefore fail only at the final manifest write, after the video blobs were already written, which leaves partial state behind in RECORDINGS_DIR. Please reject/strip NULs up front and derive every sibling path from safeScreenFileName.

nit: cleaner if the sanitized name becomes the single source of truth
 function sanitizeFileName(fileName: string): string {
 	// Strip any path separators and parent-directory references to prevent traversal
-	const baseName = path.basename(fileName);
+	const baseName = path.basename(fileName.replace(/\0/g, ""));
 	if (!baseName || baseName === "." || baseName === "..") {
 		throw new Error("Invalid file name");
 	}
 	return baseName;
 }
 
 ...
-	const sessionManifestPath = path.join(
-		RECORDINGS_DIR,
-		`${path.parse(payload.screen.fileName).name}${RECORDING_SESSION_SUFFIX}`,
-	);
+	const sessionManifestPath = path.join(
+		RECORDINGS_DIR,
+		`${path.parse(safeScreenFileName).name}${RECORDING_SESSION_SUFFIX}`,
+	);

Also applies to: 315-322

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 234 - 240, sanitizeFileName currently
only calls path.basename, so embedded NULs and other unsafe characters can
survive and the manifest is still built from raw payload.screen.fileName; update
sanitizeFileName to reject or strip NULs (and other dangerous control chars) and
return a canonical safeScreenFileName, then use that safeScreenFileName
everywhere (including where the session manifest is assembled and any sibling
paths under RECORDINGS_DIR) so the manifest write and all blob writes derive
from the same sanitized name and cannot leave partial state behind; update
references that use payload.screen.fileName to instead use the returned
safeScreenFileName.

Comment on lines +275 to +277
if (!isTrustedMediaPath(normalizedVideoPath)) {
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sidecar JSON reads still skip the realpath gate.

read-binary-file() moved to isTrustedMediaPathRealpath(), but these two flows still read ${video}.session.json / ${video}.cursor.json after only a logical-path allow check. A symlinked sidecar inside RECORDINGS_DIR can still resolve outside the trusted roots and get read back into the renderer. Please realpath the file you actually open and re-validate it the same way.

Also applies to: 599-601

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 275 - 277, The code currently checks
isTrustedMediaPath(normalizedVideoPath) then directly reads sidecar files like
`${normalizedVideoPath}.session.json` and `${normalizedVideoPath}.cursor.json`,
which can be symlinked outside trusted roots; change the flow so after
constructing the actual sidecar path you call fs.realpath (or equivalent) and
validate the resolved path with isTrustedMediaPathRealpath (the same function
used for read-binary-file) before calling read-binary-file (or reading the
file). Update both the handler that uses normalizedVideoPath and the other flow
around the same logic (the flow referenced near lines 599-601) to realpath each
sidecar file and re-run the realpath-based allow check (using
isTrustedMediaPathRealpath) prior to reading to ensure the final resolved file
is inside trusted roots.

Comment on lines 343 to +345
app.whenReady().then(async () => {
// Register custom protocol for safe local media loading
registerMediaProtocol();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the electron directory and key files
find . -type f -name "main.ts" | grep electron
find . -type f -name "windows.ts" | grep electron

Repository: siddharthvaddem/openscreen

Length of output: 110


🏁 Script executed:

# Read the relevant section of electron/main.ts around lines 343-345
wc -l electron/main.ts

Repository: siddharthvaddem/openscreen

Length of output: 90


🏁 Script executed:

# Look for registerMediaProtocol and registerSchemesAsPrivileged
rg "registerMediaProtocol|registerSchemesAsPrivileged" --no-heading -B 2 -A 2

Repository: siddharthvaddem/openscreen

Length of output: 792


🏁 Script executed:

# Read the registerMediaProtocol function fully
sed -n '/^export function registerMediaProtocol/,/^}/p' electron/windows.ts | head -30

Repository: siddharthvaddem/openscreen

Length of output: 1027


🏁 Script executed:

# Read context around lines 343-350 in main.ts
sed -n '330,360p' electron/main.ts | cat -n

Repository: siddharthvaddem/openscreen

Length of output: 1405


🏁 Script executed:

# Check if there are any scheme registrations at module level or app startup
rg "protocol\." electron/ -B 1 -A 1

Repository: siddharthvaddem/openscreen

Length of output: 258


🏁 Script executed:

# Check Electron version and dependencies
cat package.json | grep -A 5 -B 5 "electron"

Repository: siddharthvaddem/openscreen

Length of output: 1454


🌐 Web query:

Electron protocol.handle() timing requirements app.whenReady()

💡 Result:

In Electron, the timing requirements for using protocol.handle in relation to app.whenReady are as follows: - protocol.handle(scheme, handler) must be called after app.whenReady resolves (i.e., after the 'ready' event). The official Electron documentation states: "All methods unless specified can only be used after the ready event of the app module gets emitted." All examples in the docs place protocol.handle inside app.whenReady.then( => { ... }). - For custom schemes requiring privileges (e.g., standard: true, secure: true, supportFetchAPI: true), you must first call protocol.registerSchemesAsPrivileged([{ scheme: 'your-scheme', privileges: { ... } }]) before app.whenReady. This method "can only be used before the ready event ... and can be called only once." Standard pattern from docs: protocol.registerSchemesAsPrivileged([ { scheme: 'app', privileges: { standard: true, secure: true, supportFetchAPI: true } } ]); app.whenReady.then( => { protocol.handle('app', (req) => { /* handler */ }); }); Calling protocol.handle before app.whenReady will fail silently or not work, as the protocol module is not fully initialized until after ready. For built-in schemes like 'file:', additional fixes apply in older versions, but timing remains post-ready.

Citations:


🌐 Web query:

Electron protocol.registerSchemesAsPrivileged vs protocol.handle() differences

💡 Result:

protocol.registerSchemesAsPrivileged and protocol.handle are complementary methods in Electron's protocol module for handling custom protocols. protocol.registerSchemesAsPrivileged(customSchemes) must be called BEFORE the 'ready' event of the app module, and only once (recently made tolerant of multiple calls by accumulating schemes). It registers custom schemes with special privileges like standard (RFC 3986 URI syntax for proper relative/absolute URL resolution), secure, bypassCSP, supportFetchAPI, allowServiceWorkers, stream, and codeCache. Without these privileges (especially 'standard: true'), custom schemes behave like 'file:' (no relative URLs, limited APIs). Example: protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { standard: true, secure: true, supportFetchAPI: true } }]) protocol.handle(scheme, handler) is called AFTER 'ready', registers the actual handler function for the scheme that receives a GlobalRequest and returns a GlobalResponse (or Promise). It replaces deprecated registerProtocol and interceptProtocol methods (deprecated since v25+). Example: protocol.handle('app', (request) => new Response(...)) Key differences and usage: - Timing: registerSchemesAsPrivileged pre-ready (setup privileges), handle post-ready (attach handler). - Purpose: Former grants privileges (required for full functionality like fetch, ServiceWorkers, streaming); latter defines request handling logic. - Both needed for privileged custom protocols: Register scheme with privileges first, then handle it. Protocols are session-specific; use session.protocol for custom sessions. - handle supports modern Web API Response objects; older methods used callbacks with paths/buffers/strings. Older methods (registerFileProtocol etc.) are deprecated in favor of handle.

Citations:


🏁 Script executed:

# Check webSecurity settings in windows.ts
rg "webSecurity" electron/

Repository: siddharthvaddem/openscreen

Length of output: 52


🏁 Script executed:

# Check how app-media:// URLs are used in assetPath.ts
cat src/lib/assetPath.ts

Repository: siddharthvaddem/openscreen

Length of output: 1280


🏁 Script executed:

# Search for window creation and webPreferences configuration
rg "webPreferences|new BrowserWindow" electron/ -A 10

Repository: siddharthvaddem/openscreen

Length of output: 2905


🏁 Script executed:

# Look for where windows are created to see full configuration
rg "createWindow|createEditorWindow" electron/windows.ts -A 20 | head -60

Repository: siddharthvaddem/openscreen

Length of output: 593


🏁 Script executed:

# Check all window configurations more thoroughly
sed -n '/webPreferences/,/},/p' electron/windows.ts | head -40

Repository: siddharthvaddem/openscreen

Length of output: 517


🏁 Script executed:

# Check if webSecurity is set anywhere in the codebase
rg "webSecurity" --no-ignore -r .

Repository: siddharthvaddem/openscreen

Length of output: 52


🏁 Script executed:

# Check if there are any explicit security settings or defaults
rg "contextIsolation|sandbox|nodeIntegration" electron/ | grep -i "true\|false"

Repository: siddharthvaddem/openscreen

Length of output: 351


Need to register app-media:// scheme with privileges before app.ready().

With webSecurity enabled by default and app-media:// now being used for asset loading, the current protocol.handle() inside whenReady() is incomplete. Electron requires protocol.registerSchemesAsPrivileged() to be called before the ready event for custom schemes to behave properly with security enabled. Without it, the scheme won't support fetch APIs or modern URL handling, which can cause weird failures.

Add this at the top level of main.ts (before app.whenReady()):

protocol.registerSchemesAsPrivileged([
  {
    scheme: 'app-media',
    privileges: {
      standard: true,
      secure: true,
      supportFetchAPI: true,
    }
  }
]);

Then keep the registerMediaProtocol() handler call inside whenReady() as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/main.ts` around lines 343 - 345, Register the custom app-media
scheme with privileged permissions before the app is ready: add a top-level call
to protocol.registerSchemesAsPrivileged(...) specifying scheme 'app-media' with
privileges standard, secure, and supportFetchAPI (so fetch/URL handling works),
then leave the existing registerMediaProtocol() call inside app.whenReady().
Ensure the registerSchemesAsPrivileged invocation occurs before app.whenReady()
and references the same 'app-media' scheme used by registerMediaProtocol().

return new Response("Forbidden", { status: 403 });
}

return net.fetch(pathToFileURL(realResolved).toString());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the electron/windows.ts file
find . -name "windows.ts" -path "*/electron/*" -type f

Repository: siddharthvaddem/openscreen

Length of output: 91


🏁 Script executed:

# Let's also check the general structure to understand the repo
ls -la

Repository: siddharthvaddem/openscreen

Length of output: 1987


🏁 Script executed:

# Examine the electron/windows.ts file, focusing on line 63 and surrounding context
cat -n ./electron/windows.ts

Repository: siddharthvaddem/openscreen

Length of output: 6818


🏁 Script executed:

# Let's check the package.json to see what version of Electron is being used
cat package.json | grep -A 2 -B 2 electron

Repository: siddharthvaddem/openscreen

Length of output: 1067


🏁 Script executed:

# Search for any other protocol handlers or media serving code in the project
rg "protocol\.handle|net\.fetch" -A 3 -B 3

Repository: siddharthvaddem/openscreen

Length of output: 694


🏁 Script executed:

# Check if there are any tests for the media protocol handler
rg "registerMediaProtocol|app-media" -A 5 -B 5

Repository: siddharthvaddem/openscreen

Length of output: 2566


🏁 Script executed:

# Search for any Range header usage or HTTP tests
rg "Range|range|seek|scrub" -i

Repository: siddharthvaddem/openscreen

Length of output: 16319


🏁 Script executed:

# Check the main.ts to see how registerMediaProtocol is used
find . -name "main.ts" -path "*/electron/*" -type f -exec cat {} \;

Repository: siddharthvaddem/openscreen

Length of output: 10750


🏁 Script executed:

# Check Electron version and look for any fetch-related utilities or patterns
grep -r "net\.fetch" --include="*.ts" --include="*.js"

Repository: siddharthvaddem/openscreen

Length of output: 149


🏁 Script executed:

# Look for any HTTP header handling or request forwarding patterns in the codebase
rg "headers|Headers" --type ts --type js -A 2 -B 2 | head -50

Repository: siddharthvaddem/openscreen

Length of output: 52


🌐 Web query:

Electron protocol.handle request object properties headers

💡 Result:

In Electron's protocol.handle(scheme, handler), the request object passed to the handler (typed as GlobalRequest) has the following properties, including headers: - url: string - The full URL of the request. - method: string - The HTTP method (e.g., 'GET', 'POST'). - referrer: string - The referrer URL. - headers: Record<string, string> - An object containing the request headers, where keys are header names and values are strings. - uploadData: UploadData[] (optional) - Array of upload data for POST requests. - body: (inferred from examples) - The request body, usable for forwarding with net.fetch. This structure aligns with the Fetch API's Request object (per MDN references in docs), allowing usage like net.fetch('https://...', { method: req.method, headers: req.headers, body: req.body }). The headers is a plain object mapping header names to string values, as shown in official examples and type definitions. Note: protocol.handle uses modern Request-like objects (introduced ~v25+), replacing deprecated ProtocolRequest from older register*Protocol APIs.

Citations:


🌐 Web query:

Electron net.fetch forward headers Range request

💡 Result:

Electron's net.fetch uses Chromium's native networking stack and behaves like the web fetch API in the renderer process. It forwards custom request headers, including the Range header, to the server when specified in the init options. To use a Range request: js const { net } = require('electron'); async function fetchRange(url, start, end) { const response = await net.fetch(url, { headers: { Range: bytes=${start}-${end} } }); if (response.status === 206) { const data = await response.arrayBuffer; // Handle partial content } } Key points from official docs and sources: - net.fetch(input, init) accepts standard RequestInit, where headers can include Range (Electron net docs). - As it mirrors renderer fetch, Range headers are preserved and sent, similar to Chromium fetch behavior (WPT tests passed in implementation PR with no Range-specific exclusions). - Common use case: Forwarding intercepted requests in protocol.handle, where request.headers.get('Range') is accessible and net.fetch(request) passes all headers including Range to the origin (e.g., file:// for video seeking). For custom protocols serving videos/audio: - Register with supportFetchAPI: true, stream: true privileges. - In handler: Check request.headers.get('Range'), parse bytes, respond with 206 status, Content-Range, Accept-Ranges: bytes. - Or forward to net.fetch('file://path') which handles Range automatically if server supports it. No known bugs or limitations for direct net.fetch Range support as of 2026; issues are typically with custom protocol responses not setting proper range headers (e.g., #38749, #40447 resolved with privileges). Headers are downcased internally but functional.

Citations:


🏁 Script executed:

# Check if there are any tests for the protocol handler
rg "registerMediaProtocol|app-media" -r tests/ --type ts

Repository: siddharthvaddem/openscreen

Length of output: 421


🏁 Script executed:

# Look for any video playback or media testing
rg "HTMLVideoElement|video" --type ts tests/ -A 2 -B 2 | head -40

Repository: siddharthvaddem/openscreen

Length of output: 2567


🏁 Script executed:

# Check if there's actual usage of video/media playback in the app
rg "<video|HTMLVideoElement" src/ --type tsx --type ts -B 2 -A 2 | head -50

Repository: siddharthvaddem/openscreen

Length of output: 97


🏁 Script executed:

# Search for video element usage in source files
rg "video" src/ --type ts -i | grep -i "video" | head -20

Repository: siddharthvaddem/openscreen

Length of output: 1616


🏁 Script executed:

# Check how the app-media protocol is being used in the renderer
rg "app-media" src/ -A 3 -B 3

Repository: siddharthvaddem/openscreen

Length of output: 642


🏁 Script executed:

# Look at the VideoPlayback component which handles video playback
find src -name "*VideoPlayback*" -o -name "*video*" -type f | head -10

Repository: siddharthvaddem/openscreen

Length of output: 246


🏁 Script executed:

# Read the VideoPlayback.tsx to understand how the app plays video
cat -n src/components/video-editor/VideoPlayback.tsx | head -100

Repository: siddharthvaddem/openscreen

Length of output: 3846


🏁 Script executed:

# Check the full VideoPlayback file
wc -l src/components/video-editor/VideoPlayback.tsx

Repository: siddharthvaddem/openscreen

Length of output: 120


🏁 Script executed:

# Look for where the videoPath is used in VideoPlayback
rg "videoPath" src/components/video-editor/VideoPlayback.tsx -A 3 -B 3 | head -80

Repository: siddharthvaddem/openscreen

Length of output: 846


🏁 Script executed:

# Search for VideoSource usage in the file
rg "VideoSource|video\.src" src/components/video-editor/VideoPlayback.tsx -A 5 -B 5

Repository: siddharthvaddem/openscreen

Length of output: 674


🏁 Script executed:

# Look at how the video is actually loaded and where the app-media protocol is invoked
rg "app-media" -A 2 -B 2 | grep -v node_modules

Repository: siddharthvaddem/openscreen

Length of output: 762


🏁 Script executed:

# Check if there are any tests that specifically test video playback or seeking
find tests -name "*.spec.ts" -exec grep -l "video\|seek\|Range" {} \;

Repository: siddharthvaddem/openscreen

Length of output: 98


🏁 Script executed:

# Let's check the actual Electron version to confirm Range support
grep -A 2 '"electron"' package.json

Repository: siddharthvaddem/openscreen

Length of output: 164


🏁 Script executed:

# Quick confirmation: check that the video element indeed triggers Range requests in browsers
rg "currentTime|seeking|seeked" src/components/video-editor/VideoPlayback.tsx | head -5

Repository: siddharthvaddem/openscreen

Length of output: 189


Forward request headers to net.fetch to preserve Range requests.

line 63 drops headers when delegating to net.fetch(), so Range requests from video seeking get lost. When the browser tries to scrub a large recording, it ends up fetching the entire file instead of just the chunk it needs, which is kinda terrible for seek performance. Pass through method, headers, and body from the incoming request:

Example fix
return net.fetch(pathToFileURL(realResolved).toString(), {
  method: request.method,
  headers: request.headers,
  body: request.body,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/windows.ts` at line 63, The current net.fetch call in the route
handler drops incoming request metadata and breaks Range requests; update the
call that returns net.fetch(pathToFileURL(realResolved).toString()) to forward
the incoming request's method, headers, and body (e.g., use request.method,
request.headers, request.body) so Range and other header-dependent behavior is
preserved when delegating to net.fetch; locate the usage in electron/windows.ts
where realResolved, request, and net.fetch are referenced and add those options
to the net.fetch invocation.

Comment on lines +13 to +24
function computeBitrate(width: number, height: number, frameRate: number) {
const pixels = width * height;
const highFrameRateBoost = frameRate >= HIGH_FRAME_RATE_THRESHOLD ? HIGH_FRAME_RATE_BOOST : 1;

if (pixels >= FOUR_K_PIXELS) {
return Math.round(BITRATE_4K * highFrameRateBoost);
}
if (pixels >= QHD_PIXELS) {
return Math.round(BITRATE_QHD * highFrameRateBoost);
}
return Math.round(BITRATE_BASE * highFrameRateBoost);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This helper doesn’t match useScreenRecorder today.

The test version takes frameRate, but src/hooks/useScreenRecorder.ts:132-146 still keys off TARGET_FRAME_RATE. So the 30fps/59fps assertions can pass here while runtime still always applies the boost. Please test the real helper, or align the runtime implementation first.

Also applies to: 27-49

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useScreenRecorder.test.ts` around lines 13 - 24, The test's
computeBitrate helper accepts frameRate and conditions on
HIGH_FRAME_RATE_THRESHOLD, but the runtime in useScreenRecorder.ts still uses
TARGET_FRAME_RATE (so the boost is always applied); fix by making the runtime
computeBitrate behavior match the test: update useScreenRecorder's bitrate
calculation (the code around useScreenRecorder function lines that reference
TARGET_FRAME_RATE and HIGH_FRAME_RATE_BOOST) to accept or derive the actual
frameRate and apply HIGH_FRAME_RATE_BOOST only when frameRate >=
HIGH_FRAME_RATE_THRESHOLD, or alternatively change the test to call the real
helper from useScreenRecorder.ts so both use the same computeBitrate
implementation.

Comment on lines +554 to +580
activeScreenRecorder.recorder.addEventListener(
"error",
() => {
// Suppress auto-finalize to prevent the stop listener from racing this cleanup
allowAutoFinalize.current = false;
if (screenRecorder.current === activeScreenRecorder) {
screenRecorder.current = null;
}
if (webcamRecorder.current === activeWebcamRecorder) {
webcamRecorder.current = null;
}
if (activeWebcamRecorder?.recorder.state === "recording") {
try {
activeWebcamRecorder.recorder.stop();
} catch {
// Webcam recorder may already be stopped.
}
}
teardownMedia();
setRecording(false);
window.electronAPI?.setRecordingState(false);
if (finalizingRecordingId.current === activeRecordingId) {
finalizingRecordingId.current = null;
}
},
{ once: true },
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/hooks/useScreenRecorder.ts | head -100

Repository: siddharthvaddem/openscreen

Length of output: 4205


🏁 Script executed:

cat -n src/hooks/useScreenRecorder.ts | sed -n '54,90p'

Repository: siddharthvaddem/openscreen

Length of output: 1473


🏁 Script executed:

cat -n src/hooks/useScreenRecorder.ts | sed -n '540,600p'

Repository: siddharthvaddem/openscreen

Length of output: 2456


Move the error listener to recorder creation time.

There's a window between recorder.start() (line 85) and the error listener attachment (line 554) where an error event would only trigger the onerror callback that rejects recordedBlobPromise, skipping all the cleanup logic you've got in the error listener—teardown, ref nulling, webcam stop, etc. If that promise rejection goes unconsumed, it'll bite you. Hook up that error listener inside createRecorderHandle() before calling start(), and explicitly handle the promise rejection on the error path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useScreenRecorder.ts` around lines 554 - 580, The error listener is
attached too late—move the cleanup handler to recorder creation so errors
between recorder.start() and the current listener don't bypass teardown; inside
createRecorderHandle(), attach the same "error" listener to
activeScreenRecorder.recorder (the logic that sets allowAutoFinalize.current =
false, clears screenRecorder.current/webcamRecorder.current, stops
activeWebcamRecorder.recorder if recording, calls teardownMedia(), calls
setRecording(false) and window.electronAPI?.setRecordingState(false), and clears
finalizingRecordingId.current when it matches activeRecordingId) before calling
recorder.start(), and also ensure any promise(s) like recordedBlobPromise that
may reject on error have their rejection explicitly consumed/handled in the
error path to avoid unhandled rejections.

Comment on lines +81 to +98
it("bounds the vertical-stack layout within maxContentSize when webcam is present", () => {
const layout = computeCompositeLayout({
canvasSize: { width: 1920, height: 1080 },
maxContentSize: { width: 1536, height: 864 },
screenSize: { width: 1920, height: 1080 },
webcamSize: { width: 1280, height: 720 },
layoutPreset: "vertical-stack",
});

expect(layout).not.toBeNull();
// Screen and webcam should be bounded within maxContentSize, not filling the canvas
expect(layout!.screenRect.width).toBeLessThanOrEqual(1536);
expect(layout!.screenRect.height + (layout!.webcamRect?.height ?? 0)).toBeLessThanOrEqual(
864 + 1,
); // +1 for rounding
// Should be centered in canvas
expect(layout!.screenRect.x).toBeGreaterThan(0);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Behavior/test contract mismatch with current vertical-stack implementation

nice regression intent, but Line 92–Line 95 currently assert a contract that computeCompositeLayout (stack branch) doesn’t implement yet. In src/lib/compositeLayout.ts (Line 150–Line 184), vertical-stack still uses canvasWidth/canvasHeight and ignores maxContentSize, so this will fail unless that implementation is updated in the same PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/compositeLayout.test.ts` around lines 81 - 98, The test assumes the
"vertical-stack" branch of computeCompositeLayout honors maxContentSize but the
current implementation still uses canvasSize; update the vertical-stack branch
in computeCompositeLayout to constrain screenRect and webcamRect dimensions
using maxContentSize.width and maxContentSize.height (apply the same
stacking/spacing and rounding logic currently used for canvasSize), ensure total
stacked height (screenRect.height + webcamRect.height) is bounded by
maxContentSize.height (allowing the existing +1 rounding tolerance), and center
the composed content within the canvas so screenRect.x is > 0; keep existing
behavior for other presets unchanged.

Comment on lines +90 to +98
expect(layout).not.toBeNull();
// Screen and webcam should be bounded within maxContentSize, not filling the canvas
expect(layout!.screenRect.width).toBeLessThanOrEqual(1536);
expect(layout!.screenRect.height + (layout!.webcamRect?.height ?? 0)).toBeLessThanOrEqual(
864 + 1,
); // +1 for rounding
// Should be centered in canvas
expect(layout!.screenRect.x).toBeGreaterThan(0);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add an explicit webcamRect assertion (lowkey safer)

On Line 93, layout!.webcamRect?.height ?? 0 makes this pass path a bit too forgiving. Since this case is explicitly “webcam is present,” assert non-null first so a missing webcam rect can’t slip through.

nit: cleaner assertion sequence
 		expect(layout).not.toBeNull();
+		expect(layout!.webcamRect).not.toBeNull();
 		// Screen and webcam should be bounded within maxContentSize, not filling the canvas
 		expect(layout!.screenRect.width).toBeLessThanOrEqual(1536);
-		expect(layout!.screenRect.height + (layout!.webcamRect?.height ?? 0)).toBeLessThanOrEqual(
+		expect(layout!.screenRect.height + layout!.webcamRect!.height).toBeLessThanOrEqual(
 			864 + 1,
 		); // +1 for rounding
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(layout).not.toBeNull();
// Screen and webcam should be bounded within maxContentSize, not filling the canvas
expect(layout!.screenRect.width).toBeLessThanOrEqual(1536);
expect(layout!.screenRect.height + (layout!.webcamRect?.height ?? 0)).toBeLessThanOrEqual(
864 + 1,
); // +1 for rounding
// Should be centered in canvas
expect(layout!.screenRect.x).toBeGreaterThan(0);
});
expect(layout).not.toBeNull();
expect(layout!.webcamRect).not.toBeNull();
// Screen and webcam should be bounded within maxContentSize, not filling the canvas
expect(layout!.screenRect.width).toBeLessThanOrEqual(1536);
expect(layout!.screenRect.height + layout!.webcamRect!.height).toBeLessThanOrEqual(
864 + 1,
); // +1 for rounding
// Should be centered in canvas
expect(layout!.screenRect.x).toBeGreaterThan(0);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/compositeLayout.test.ts` around lines 90 - 98, The test is too
permissive by using layout!.webcamRect?.height ?? 0; explicitly assert that the
webcam rect exists before measuring its height: add an assertion like
expect(layout!.webcamRect).not.toBeNull() (or truthy) and then use
layout!.webcamRect!.height in the subsequent constraint check (the code paths
using layout and webcamRect are the targets to update).

Comment on lines +3 to +12
// Replicate the isAllowedExternalUrl logic for testing
function isAllowedExternalUrl(url: string): boolean {
if (!url || typeof url !== "string") return false;
try {
const parsed = new URL(url);
return parsed.protocol === "https:" || parsed.protocol === "http:";
} catch {
return false;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

This security test is only exercising its own copy.

electron/ipc/handlers.ts already has the production isAllowedExternalUrl() implementation. Re-defining it here means a future regression in the IPC handler still goes green in tests, which is lowkey risky for a security boundary. Please extract this helper into a shared module and import the real implementation instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/securityValidation.test.ts` around lines 3 - 12, The test defines its
own isAllowedExternalUrl which masks regressions; extract the real
isAllowedExternalUrl into a shared utility module and have both the IPC handler
and this test import that shared function instead of redefining it. Move the
current implementation (the URL parsing + protocol check) into the new shared
export, update the IPC handler to import and use that export, and change
src/lib/securityValidation.test.ts to import isAllowedExternalUrl from the
shared module so the test exercises the actual production logic.

…-in-folder, and exports

- set-current-video-path: use isPathAllowed (checks approvedPaths + trusted dirs)
  instead of isTrustedMediaPath so user-picked external videos are accepted
- reveal-in-folder: use isPathAllowed so exported files in Downloads etc. can be revealed
- save-exported-video: approve the save path so reveal-in-folder works after export
- Remove unused resolveRecordingOutputPath (replaced by sanitizeFileName)
- Remove vertical-stack layout test that belongs with the layout fix (PR siddharthvaddem#361)

Addresses review feedback from Codex, CodeRabbit, and Gemini.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
electron/ipc/handlers.ts (1)

917-941: 🧹 Nitpick | 🔵 Trivial

minor inconsistency: passing raw videoPath instead of normalizedVideoPath to session loader.

line 919 validates normalizedVideoPath, but line 925 passes the original videoPath to loadRecordedSessionForVideoPath. if the input was a file:// URL, these would differ, and the session load would re-normalize internally anyway. kinda redundant but also kinda risky if normalization behavior ever diverges.

-		const restoredSession = await loadRecordedSessionForVideoPath(videoPath);
+		const restoredSession = await loadRecordedSessionForVideoPath(normalizedVideoPath);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 917 - 941, The handler registered for
"set-current-video-path" validates and produces normalizedVideoPath using
normalizeVideoSourcePath but still calls
loadRecordedSessionForVideoPath(videoPath); change that call to pass
normalizedVideoPath instead (i.e., use
loadRecordedSessionForVideoPath(normalizedVideoPath)) so session lookup uses the
same canonical path you validated, leaving the rest of the logic
(approveFilePath, setCurrentRecordingSessionState) unchanged.
♻️ Duplicate comments (5)
electron/ipc/handlers.ts (5)

255-261: ⚠️ Potential issue | 🟠 Major

manifest file read still skips realpath validation.

you check isTrustedMediaPath(normalizedVideoPath) but then read manifestPath without resolving symlinks. if someone plants a symlink at /recordings/foo.session.json → /sensitive/data.json, the logical path passes but you read outside the trusted root.

suggested fix
 	try {
 		const manifestPath = getSessionManifestPathForVideo(normalizedVideoPath);
+		let realManifestPath: string;
+		try {
+			realManifestPath = await fs.realpath(manifestPath);
+			if (!isTrustedMediaPath(realManifestPath)) {
+				return null;
+			}
+		} catch {
+			// File doesn't exist
+			return null;
+		}
-		const content = await fs.readFile(manifestPath, "utf-8");
+		const content = await fs.readFile(realManifestPath, "utf-8");

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 255 - 261, The manifest is read via
manifestPath from getSessionManifestPathForVideo(normalizedVideoPath) without
resolving symlinks, so an attacker could point that manifest to an outside file;
resolve the manifest's real path (e.g., fs.realpath or equivalent) after
computing manifestPath and before fs.readFile, then re-run the trust check (use
isTrustedMediaPath or a new isTrustedMediaPathForManifest) against the resolved
manifest path and only proceed to fs.readFile if the resolved path is trusted;
ensure you reference getSessionManifestPathForVideo, manifestPath,
normalizedVideoPath, isTrustedMediaPath and fs.readFile when making the change.

579-585: ⚠️ Potential issue | 🟠 Major

cursor telemetry read also skips realpath validation on the sidecar file.

same issue as manifest: you validate targetVideoPath but then read ${targetVideoPath}.cursor.json without resolving symlinks. need to realpath the telemetry file itself before reading.

 const telemetryPath = `${targetVideoPath}.cursor.json`;
+let realTelemetryPath: string;
 try {
+	realTelemetryPath = await fs.realpath(telemetryPath);
+	if (!isTrustedMediaPath(realTelemetryPath)) {
+		return { success: true, samples: [] };
+	}
-	const content = await fs.readFile(telemetryPath, "utf-8");
+	const content = await fs.readFile(realTelemetryPath, "utf-8");

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 579 - 585, The telemetry sidecar file
is read without realpath/symlink validation: before calling fs.readFile on
telemetryPath (the `${targetVideoPath}.cursor.json` variable) resolve its real
path (e.g., fs.realpath/realpathSync) and run isTrustedMediaPath against the
resolved path; if the resolved telemetry path is not trusted, return the same
safe response ({ success: true, samples: [] }) and only then proceed to read the
file and parse its contents. Ensure you update the telemetryPath usage in this
block (and any related error handling) to use the resolved path.

186-199: ⚠️ Potential issue | 🟠 Major

isTrustedMediaPath still doesn't consult approvedPaths, breaking user-picked external files.

tracing the flow: user picks file from Downloads via open-video-file-picker → file gets added to approvedPathsset-current-video-path uses isPathAllowed (passes) → but read-binary-file uses isTrustedMediaPathRealpath (fails because Downloads isn't a trusted root).

either isTrustedMediaPath needs to also check approvedPaths, or you need a separate function that combines both checks for the read handlers.

suggested fix
 function isTrustedMediaPath(filePath: string): boolean {
 	if (!filePath || typeof filePath !== "string") {
 		return false;
 	}
 	const normalized = normalizePath(filePath);
+
+	// Check explicitly approved paths first
+	if (approvedPaths.has(normalized)) {
+		return true;
+	}

 	const trustedRoots: string[] = [normalizePath(RECORDINGS_DIR)];

 	if (currentProjectPath) {
 		trustedRoots.push(normalizePath(path.dirname(currentProjectPath)));
 	}

 	return trustedRoots.some((root) => normalized === root || normalized.startsWith(root + path.sep));
 }

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 186 - 199, isTrustedMediaPath
currently only checks trustedRoots and thus rejects user-approved files; update
isTrustedMediaPath to also consult the approvedPaths store (or call the existing
isPathAllowed helper) so that user-picked external files accepted by
set-current-video-path and open-video-file-picker are treated as trusted by
read-binary-file and isTrustedMediaPathRealpath; ensure you compare
canonical/normalized (or realpath-resolved) values when checking approvedPaths
to avoid symlink/path variation issues and reference isTrustedMediaPath,
approvedPaths (or isPathAllowed), isTrustedMediaPathRealpath, read-binary-file,
set-current-video-path, and open-video-file-picker when making the change.

322-325: ⚠️ Potential issue | 🟡 Minor

manifest path still derives from raw payload.screen.fileName instead of safeScreenFileName.

if sanitization ever diverges (or you add more stripping later), this creates inconsistency. safer to use the same sanitized name everywhere:

 const sessionManifestPath = path.join(
 	RECORDINGS_DIR,
-	`${path.parse(payload.screen.fileName).name}${RECORDING_SESSION_SUFFIX}`,
+	`${path.parse(safeScreenFileName).name}${RECORDING_SESSION_SUFFIX}`,
 );

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 322 - 325, The manifest path is built
from the raw payload.screen.fileName causing inconsistency with sanitized names;
change the sessionManifestPath construction to use the already-computed
safeScreenFileName (or, if safeScreenFileName excludes extension, use it
directly; otherwise use path.parse(safeScreenFileName).name) instead of
path.parse(payload.screen.fileName).name so RECORDINGS_DIR +
RECORDING_SESSION_SUFFIX always aligns with the sanitized filename.

214-221: ⚠️ Potential issue | 🟡 Minor

NULs and control chars still pass through sanitizeFileName.

path.basename handles path traversal but embedded \0 bytes survive. depending on the OS/filesystem, this can cause string truncation or weird behavior. safer to strip them:

suggested fix
 function sanitizeFileName(fileName: string): string {
-	// Strip any path separators and parent-directory references to prevent traversal
-	const baseName = path.basename(fileName);
+	// Strip NULs, control chars, path separators, and parent-directory references
+	const cleaned = fileName.replace(/[\x00-\x1f]/g, "");
+	const baseName = path.basename(cleaned);
 	if (!baseName || baseName === "." || baseName === "..") {
 		throw new Error("Invalid file name");
 	}
 	return baseName;
 }

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 214 - 221, sanitizeFileName currently
uses path.basename but allows embedded NULs/control characters; update the
sanitizeFileName(fileName: string) function to remove or reject control
characters (at minimum NUL \x00) after computing baseName—e.g., strip all
characters in the control range [\x00-\x1F\x7F] or throw an error if any are
present—so the function returns a safe filename without embedded NULs/control
bytes that could truncate or misbehave on some OS/filesystems.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@electron/ipc/handlers.ts`:
- Around line 917-941: The handler registered for "set-current-video-path"
validates and produces normalizedVideoPath using normalizeVideoSourcePath but
still calls loadRecordedSessionForVideoPath(videoPath); change that call to pass
normalizedVideoPath instead (i.e., use
loadRecordedSessionForVideoPath(normalizedVideoPath)) so session lookup uses the
same canonical path you validated, leaving the rest of the logic
(approveFilePath, setCurrentRecordingSessionState) unchanged.

---

Duplicate comments:
In `@electron/ipc/handlers.ts`:
- Around line 255-261: The manifest is read via manifestPath from
getSessionManifestPathForVideo(normalizedVideoPath) without resolving symlinks,
so an attacker could point that manifest to an outside file; resolve the
manifest's real path (e.g., fs.realpath or equivalent) after computing
manifestPath and before fs.readFile, then re-run the trust check (use
isTrustedMediaPath or a new isTrustedMediaPathForManifest) against the resolved
manifest path and only proceed to fs.readFile if the resolved path is trusted;
ensure you reference getSessionManifestPathForVideo, manifestPath,
normalizedVideoPath, isTrustedMediaPath and fs.readFile when making the change.
- Around line 579-585: The telemetry sidecar file is read without
realpath/symlink validation: before calling fs.readFile on telemetryPath (the
`${targetVideoPath}.cursor.json` variable) resolve its real path (e.g.,
fs.realpath/realpathSync) and run isTrustedMediaPath against the resolved path;
if the resolved telemetry path is not trusted, return the same safe response ({
success: true, samples: [] }) and only then proceed to read the file and parse
its contents. Ensure you update the telemetryPath usage in this block (and any
related error handling) to use the resolved path.
- Around line 186-199: isTrustedMediaPath currently only checks trustedRoots and
thus rejects user-approved files; update isTrustedMediaPath to also consult the
approvedPaths store (or call the existing isPathAllowed helper) so that
user-picked external files accepted by set-current-video-path and
open-video-file-picker are treated as trusted by read-binary-file and
isTrustedMediaPathRealpath; ensure you compare canonical/normalized (or
realpath-resolved) values when checking approvedPaths to avoid symlink/path
variation issues and reference isTrustedMediaPath, approvedPaths (or
isPathAllowed), isTrustedMediaPathRealpath, read-binary-file,
set-current-video-path, and open-video-file-picker when making the change.
- Around line 322-325: The manifest path is built from the raw
payload.screen.fileName causing inconsistency with sanitized names; change the
sessionManifestPath construction to use the already-computed safeScreenFileName
(or, if safeScreenFileName excludes extension, use it directly; otherwise use
path.parse(safeScreenFileName).name) instead of
path.parse(payload.screen.fileName).name so RECORDINGS_DIR +
RECORDING_SESSION_SUFFIX always aligns with the sanitized filename.
- Around line 214-221: sanitizeFileName currently uses path.basename but allows
embedded NULs/control characters; update the sanitizeFileName(fileName: string)
function to remove or reject control characters (at minimum NUL \x00) after
computing baseName—e.g., strip all characters in the control range
[\x00-\x1F\x7F] or throw an error if any are present—so the function returns a
safe filename without embedded NULs/control bytes that could truncate or
misbehave on some OS/filesystems.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b1b9dc97-a78d-4726-9e5c-5a9d3644dafc

📥 Commits

Reviewing files that changed from the base of the PR and between 2078088 and 0078117.

📒 Files selected for processing (1)
  • electron/ipc/handlers.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant