feat: add deeplink controls and Raycast extension#1838
Conversation
| fn parse_recording_mode(value: String) -> Result<RecordingMode, ActionParseFromUrlError> { | ||
| serde_json::from_str::<RecordingMode>(&format!("\"{value}\"")) | ||
| .map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string())) | ||
| } |
There was a problem hiding this comment.
parse_recording_mode embeds the URL-decoded value directly into a JSON string literal with format!("\"{value}\"") without escaping. A value containing a backslash (e.g., mode=studio%5C) produces "studio\" — an unterminated JSON string — causing a misleading ParseFailed error rather than a clear "invalid mode" message. Use serde_json::to_string to produce a properly escaped JSON string.
| fn parse_recording_mode(value: String) -> Result<RecordingMode, ActionParseFromUrlError> { | |
| serde_json::from_str::<RecordingMode>(&format!("\"{value}\"")) | |
| .map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string())) | |
| } | |
| fn parse_recording_mode(value: String) -> Result<RecordingMode, ActionParseFromUrlError> { | |
| let json = serde_json::to_string(&value) | |
| .map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?; | |
| serde_json::from_str::<RecordingMode>(&json) | |
| .map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string())) | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 212-215
Comment:
`parse_recording_mode` embeds the URL-decoded value directly into a JSON string literal with `format!("\"{value}\"")` without escaping. A value containing a backslash (e.g., `mode=studio%5C`) produces `"studio\"` — an unterminated JSON string — causing a misleading `ParseFailed` error rather than a clear "invalid mode" message. Use `serde_json::to_string` to produce a properly escaped JSON string.
```suggestion
fn parse_recording_mode(value: String) -> Result<RecordingMode, ActionParseFromUrlError> {
let json = serde_json::to_string(&value)
.map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?;
serde_json::from_str::<RecordingMode>(&json)
.map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))
}
```
How can I resolve this? If you propose a fix, please make it concise.| CameraSelector::Label(label) => cameras | ||
| .iter() | ||
| .find(|camera| camera.display_name == *label) | ||
| .ok_or_else(|| format!("No camera with label \"{label}\""))? | ||
| .device_or_model_id(), |
There was a problem hiding this comment.
Label matching is case-sensitive here while
DeviceId and ModelId both use eq_ignore_ascii_case. A deeplink like cap-desktop://device/camera?label=facetime+hd+camera would fail to match a camera named FaceTime HD Camera, even though the same user intent succeeds with the other two selectors. Applying consistent case-folding avoids this surprise.
| CameraSelector::Label(label) => cameras | |
| .iter() | |
| .find(|camera| camera.display_name == *label) | |
| .ok_or_else(|| format!("No camera with label \"{label}\""))? | |
| .device_or_model_id(), | |
| CameraSelector::Label(label) => cameras | |
| .iter() | |
| .find(|camera| camera.display_name.eq_ignore_ascii_case(label)) | |
| .ok_or_else(|| format!("No camera with label \"{label}\""))? | |
| .device_or_model_id(), |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 288-292
Comment:
Label matching is case-sensitive here while `DeviceId` and `ModelId` both use `eq_ignore_ascii_case`. A deeplink like `cap-desktop://device/camera?label=facetime+hd+camera` would fail to match a camera named `FaceTime HD Camera`, even though the same user intent succeeds with the other two selectors. Applying consistent case-folding avoids this surprise.
```suggestion
CameraSelector::Label(label) => cameras
.iter()
.find(|camera| camera.display_name.eq_ignore_ascii_case(label))
.ok_or_else(|| format!("No camera with label \"{label}\""))?
.device_or_model_id(),
```
How can I resolve this? If you propose a fix, please make it concise.| for (const value of Object.values(object)) { | ||
| if (typeof value !== "string") continue; | ||
| const directMatch = value.match(/\b[0-9a-fA-F]{4}:[0-9a-fA-F]{4}\b/); | ||
| if (directMatch) return directMatch[0].toLowerCase(); | ||
| } |
There was a problem hiding this comment.
Unanchored value scan may produce false-positive model IDs
readModelId scans every string value in the object for /\b[0-9a-fA-F]{4}:[0-9a-fA-F]{4}\b/ before consulting key names. Any string field that incidentally contains a xxxx:xxxx hex-like substring (e.g., a UUID fragment, a serial number, or a firmware version tag) would be treated as a model ID and generate a wrong deeplink. The key-scoped readHexId path that follows already applies key filtering; applying similar key filtering to the direct-match scan (e.g. checking that the key contains model, vendor, product, or usb) would prevent false matches.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/lib/devices.ts
Line: 207-211
Comment:
**Unanchored value scan may produce false-positive model IDs**
`readModelId` scans every string value in the object for `/\b[0-9a-fA-F]{4}:[0-9a-fA-F]{4}\b/` before consulting key names. Any string field that incidentally contains a `xxxx:xxxx` hex-like substring (e.g., a UUID fragment, a serial number, or a firmware version tag) would be treated as a model ID and generate a wrong deeplink. The key-scoped `readHexId` path that follows already applies key filtering; applying similar key filtering to the direct-match scan (e.g. checking that the key contains `model`, `vendor`, `product`, or `usb`) would prevent false matches.
How can I resolve this? If you propose a fix, please make it concise.- use serde_json::to_string in parse_recording_mode to avoid unescaped JSON - apply eq_ignore_ascii_case to CameraSelector::Label for consistency - scope readModelId hex scan to model/vendor/product/usb keys to prevent false positives
Rebased onto CapSoftware/Cap main (181 commits ahead). Lockfile conflict resolved by taking upstream resolution strings; the raycast workspace packages don't affect the root lockfile. All other files merged automatically without conflicts.
Replace 'npx @raycast/api@latest lint' with 'ray lint', which uses the locally installed Raycast CLI from the pinned @raycast/api@1.104.17 dependency rather than fetching the unpinned latest at runtime. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Caught up on the bot feedback and did a few things: Greptile P2 fixes (already committed in the previous push, confirming they're still intact after rebasing):
Superagent LOW: Pinned the Raycast lint script to the locally-installed Superagent MEDIUM (external deeplinks controlling recording without confirmation): Noted. The deeplink surface is the intended UX — Raycast actions and system shortcuts are user-initiated. If the team wants an explicit trust gate (e.g. a one-time prompt on first deeplink use, or an opt-in setting in Preferences → Integrations), happy to add that before merge. Let me know the preferred direction. Merge conflicts: Rebased onto the latest upstream main and resolved the single |
| @@ -144,9 +352,35 @@ impl DeepLinkAction { | |||
| .await | |||
There was a problem hiding this comment.
P2: Unauthenticated custom-protocol deeplinks can trigger recording and device-control actions from outside the app
External cap-desktop:// URLs now directly start/stop recording and switch devices.
Gate recording/device deeplinks behind confirmation or a trusted nonce before executing.
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:352">
<priority>P2</priority>
<title>Unauthenticated custom-protocol deeplinks can trigger recording and device-control actions from outside the app</title>
<evidence>The new deeplink handler accepts external `cap-desktop://record/*` and `cap-desktop://device/*` URLs and executes them directly: `StartSavedRecording` calls `start_recording_from_saved_settings`, `PauseRecording`/`ResumeRecording`/`StopRecording` call the recording controls, and `SetMicrophone`/`SetCamera` change inputs. Custom URL schemes can be invoked by other local apps and by web pages via browser protocol handling, so this adds an unauthenticated path to start/stop/pause recordings or change capture devices using saved settings.</evidence>
<recommendation>Require explicit in-app user confirmation or a trusted-origin/session-bound nonce before executing recording or device-switch deeplinks. At minimum, restrict external deeplinks to focusing the app and presenting a confirmation UI for recording start/stop and device changes rather than executing immediately.</recommendation>
</violation>
</file>
Summary
cap-desktop://action?value=...andcap-desktop://signin?...behaviorapps/desktop/src-tauri/DEEPLINKS.mdplus a newapps/raycastextension withcap-controlandswitch-devicecommandsDeeplink URLs
cap-desktop://record/startcap-desktop://record/start?mode=studiocap-desktop://record/start?mode=instantcap-desktop://record/stopcap-desktop://record/pausecap-desktop://record/resumecap-desktop://record/toggle-pausecap-desktop://device/microphone?label=<device-label>cap-desktop://device/microphone?off=truecap-desktop://device/camera?model_id=<vid:pid>cap-desktop://device/camera?device_id=<unique-id>cap-desktop://device/camera?id=<unique-id>cap-desktop://device/camera?label=<display-name>cap-desktop://device/camera?off=trueValidation
cargo fmt --allcargo check -p cap-desktoppango,gdk-3.0,cairo,gdk-pixbuf-2.0./node_modules/.bin/tsc -p apps/raycast/tsconfig.json --noEmit --pretty falsenpx @raycast/api@latest lintauthorfield becauseCapSoftwareis not a valid Raycast usernameManual Testing
open "cap-desktop://..."flows or verify in-app macOS recording UI sync manually.Demo
Notes
system_profileroutput and prefers cameramodel_id, thendevice_id, then label fallback.Greptile Summary
This PR adds semantic
cap-desktop://record/*andcap-desktop://device/*deeplink routes to the Tauri desktop backend, alongside a newapps/raycastextension withcap-controlandswitch-devicecommands. Existingactionandsignindeeplinks are preserved unchanged.deeplink_actions.rs,lib.rs): new URL parser dispatches to recording and device-switch handlers, routing through the same pause/resume/stop paths already used by the in-app UI;start_recording_from_saved_settingsis extracted from the existingRequestStartRecordinglistener to share logic.devices.ts,deeplinks.ts,cap-control.tsx,switch-device.tsx): enumerates macOS audio and camera devices viasystem_profiler -json, builds deeplinks preferringmodel_id→device_id→label, and opens them viacap-desktop://scheme.#[cfg(test)]module indeeplink_actions.rspins URL shape, all new record/device parse paths, and error cases.Confidence Score: 4/5
The new deeplink routes reuse well-tested recording and device-switch paths; the refactor of start_recording_from_saved_settings is behaviour-preserving.
Three small correctness concerns exist: parse_recording_mode builds a JSON string via raw format! without escaping, camera label matching is case-sensitive while the two ID selectors use eq_ignore_ascii_case, and readModelId scans all string values for a hex pattern that could match non-ID fields. None block correct operation under normal inputs but are worth addressing.
apps/desktop/src-tauri/src/deeplink_actions.rs for mode-parsing and label-matching issues; apps/raycast/src/lib/devices.ts for the model-ID heuristic.
Important Files Changed
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "chore: fix raycast icon permissions" | Re-trigger Greptile