Skip to content
Closed
70 changes: 48 additions & 22 deletions apps/desktop/src-tauri/src/deeplink_actions.rs

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

impl TryFrom<&Url> for DeepLinkAction looks accidentally corrupted right now (multiple nested fn try_from(...) declarations + stray return Err(...) blocks). As-is it won’t compile.

I’d collapse it back to a single fn try_from(url: &Url) -> Result<...> body (keeping the macOS file:// early return) and then keep the scheme/host routing logic inside that one function.

Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,14 @@ pub enum DeepLinkAction {
mode: RecordingMode,
},
StopRecording,
#[cfg(debug_assertions)]
PauseRecording,
#[cfg(debug_assertions)]
ResumeRecording,
#[cfg(debug_assertions)]
SwitchMicrophone {
mic_label: Option<String>,
},
OpenCamera {
camera: DeviceOrModelID,
},
#[cfg(debug_assertions)]
SetCameraPreviewState {
state: CameraPreviewState,
Comment thread
BMNTR marked this conversation as resolved.
Outdated
},
Expand Down Expand Up @@ -165,20 +164,46 @@ impl TryFrom<&Url> for DeepLinkAction {
}

match url.domain() {
Comment thread
BMNTR marked this conversation as resolved.
Outdated
Some("action") => {}
Some(_) => return Err(ActionParseFromUrlError::NotAction),
None => return Err(ActionParseFromUrlError::Invalid),
Some("action") => {
let params = url
.query_pairs()
.collect::<std::collections::HashMap<_, _>>();
let json_value = params
.get("value")
.ok_or(ActionParseFromUrlError::Invalid)?;
let action: Self = serde_json::from_str(json_value)
.map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?;
Ok(action)
}

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: Sensitive recording controls exposed in production via unauthenticated deep links

Sensitive recording controls now exposed via unauthenticated deep links in production builds.

Add authorization checks or user confirmation before executing sensitive controls via deep links.

AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.

<file name="apps/desktop/src-tauri/src/deeplink_actions.rs">
<violation number="1" location="apps/desktop/src-tauri/src/deeplink_actions.rs:177">
<priority>P1</priority>
<title>Sensitive recording controls exposed in production via unauthenticated deep links</title>
<evidence>The PR removes #[cfg(debug_assertions)] guards from PauseRecording, ResumeRecording, OpenCamera, and SetCameraPreviewState, and adds direct URL path handlers (cap://pause, cap://resume, cap://switch-mic, cap://switch-camera) that allow any application or webpage to trigger these sensitive controls without authentication. The original debug guards indicate these actions were intentionally restricted from production builds.</evidence>
<recommendation>Add an authorization check before executing sensitive recording controls via deep links, such as requiring user confirmation or a signed/validated token in the URL. Alternatively, gate these actions behind debug assertions or a developer mode toggle unless there is explicit user consent for each invocation.</recommendation>
</violation>
</file>

Some("stop" | "stop-recording") => Ok(Self::StopRecording),
Some("pause" | "pause-recording") => Ok(Self::PauseRecording),
Some("resume" | "resume-recording") => Ok(Self::ResumeRecording),
Some("switch-mic" | "switch-microphone") => {
let params = url
.query_pairs()
.collect::<std::collections::HashMap<_, _>>();
let mic_label = params
Comment thread
BMNTR marked this conversation as resolved.
.get("mic_label")
.or_else(|| params.get("label"))
.map(|s| s.to_string());
Ok(Self::SwitchMicrophone { mic_label })
}
Some("open-camera" | "switch-camera") => {
let params = url
.query_pairs()
.collect::<std::collections::HashMap<_, _>>();
let device_id = params
Comment thread
BMNTR marked this conversation as resolved.
.get("id")
.or_else(|| params.get("camera"))
Comment thread
BMNTR marked this conversation as resolved.
.map(|s| s.to_string())
.ok_or(ActionParseFromUrlError::Invalid)?;
Ok(Self::OpenCamera {
camera: DeviceOrModelID::DeviceID(device_id),
})
}
Comment on lines +182 to +207

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Direct-link scheme is unregistered

When Raycast invokes a documented direct URL such as cap://pause or cap://switch-mic?label=..., the operating system cannot route it to Cap because the application registers only the cap-desktop scheme, so none of these new action handlers executes.

Knowledge Base Used: Desktop Tauri App (Rust Backend)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 178-203

Comment:
**Direct-link scheme is unregistered**

When Raycast invokes a documented direct URL such as `cap://pause` or `cap://switch-mic?label=...`, the operating system cannot route it to Cap because the application registers only the `cap-desktop` scheme, so none of these new action handlers executes.

**Knowledge Base Used:** [Desktop Tauri App (Rust Backend)](https://app.greptile.com/cap/-/custom-context/knowledge-base/capsoftware/cap/-/docs/desktop-tauri-app.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread
BMNTR marked this conversation as resolved.
Some(_) => Err(ActionParseFromUrlError::NotAction),
None => Err(ActionParseFromUrlError::Invalid),
}
Comment thread
BMNTR marked this conversation as resolved.

let params = url
.query_pairs()
.collect::<std::collections::HashMap<_, _>>();
let json_value = params
.get("value")
.ok_or(ActionParseFromUrlError::Invalid)?;
let action: Self = serde_json::from_str(json_value)
.map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?;
Ok(action)
}
}

Expand Down Expand Up @@ -244,15 +269,18 @@ impl DeepLinkAction {
DeepLinkAction::StopRecording => {
crate::recording::stop_recording(app.clone(), app.state()).await
}
#[cfg(debug_assertions)]
DeepLinkAction::PauseRecording => {
crate::recording::pause_recording(app.clone(), app.state()).await
}
#[cfg(debug_assertions)]
DeepLinkAction::ResumeRecording => {
crate::recording::resume_recording(app.clone(), app.state()).await
}
#[cfg(debug_assertions)]
DeepLinkAction::SwitchMicrophone { mic_label } => {
let state = app.state::<ArcLock<App>>();
crate::set_mic_input(state, mic_label)
.await
.map_err(|e| e.to_string())
}
DeepLinkAction::OpenCamera { camera } => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since this arm is no longer #[cfg(debug_assertions)], double-check release builds still compile: app.emit(...) is provided by tauri::Emitter, but the use tauri::Emitter; import above is still debug-only. Easiest fix is to un-gate that import or switch to UFCS (tauri::Emitter::emit(app, ...)) so the trait doesn’t need to be in scope.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since OpenCamera is now reachable in release builds, note this arm calls app.emit(...) further down. That requires the tauri::Emitter trait to be in scope, but the import at the top is still #[cfg(debug_assertions)] — release builds will fail unless you un-gate that import (or switch to tauri::Emitter::emit(app, ...)).

crate::set_camera_input(
app.clone(),
Expand Down Expand Up @@ -282,7 +310,6 @@ impl DeepLinkAction {

Ok(())
}
#[cfg(debug_assertions)]
DeepLinkAction::SetCameraPreviewState { state } => {
crate::set_camera_preview_state(app.state(), state).await
}
Expand Down Expand Up @@ -358,7 +385,6 @@ mod tests {
);
}

#[cfg(debug_assertions)]
#[test]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since the new public contract is the direct host routes (e.g. cap://pause, cap://switch-mic?label=..., cap://switch-camera?id=...), it’d be nice to add a couple small tests for those alongside the existing cap-desktop://action?... coverage.

fn parses_pause_and_resume_action_urls() {
let pause_url = Url::parse("cap-desktop://action?value=%22pause_recording%22").unwrap();
Expand Down