diff --git a/app/src/config/constants/compatibility.js b/app/src/config/constants/compatibility.js index 78aafaa4dd..3823de63bd 100644 --- a/app/src/config/constants/compatibility.js +++ b/app/src/config/constants/compatibility.js @@ -256,4 +256,12 @@ export const FEATURE_COMPATIBLE_VERSION = { [GLOBAL_CONSTANTS.APP_MODES.DESKTOP]: "26.3.19", [GLOBAL_CONSTANTS.APP_MODES.EXTENSION]: null, }, + [FEATURES.DEVELOPER_SCRIPT_MODE]: { + // RQ-2426: TODO — replace "99.99.99" with the desktop release that ships the + // devScriptMode preference handlers + sandboxing proxy (the RQ-2426 desktop PR). + // Placeholder is intentionally fail-closed: the toggle stays HIDDEN on every + // desktop version until this is set, so it never appears on an incompatible build. + [GLOBAL_CONSTANTS.APP_MODES.DESKTOP]: "99.99.99", + [GLOBAL_CONSTANTS.APP_MODES.EXTENSION]: null, // desktop-proxy-only feature + }, }; diff --git a/app/src/config/constants/sub/features.js b/app/src/config/constants/sub/features.js index c98d9e4a5b..3eeb904377 100644 --- a/app/src/config/constants/sub/features.js +++ b/app/src/config/constants/sub/features.js @@ -114,4 +114,9 @@ FEATURES.DESKTOP_BETA_PREVIEW_URL_CONFIGURATION = "desktop_beta_preview_url_conf FEATURES.SECRETS_MANAGER = "secrets_manager"; FEATURES.ALLOW_INSECURE_SSL = "allow_insecure_ssl"; +// RQ-2426: desktop-only "Developer script mode" toggle (safe sandbox vs full-access +// for programmatic code rules). Gated so it only shows on desktop versions that ship +// the devScriptMode preference handlers + sandboxing proxy. +FEATURES.DEVELOPER_SCRIPT_MODE = "developer_script_mode"; + export default FEATURES; diff --git a/app/src/features/settings/components/DesktopSettings/DesktopSettings.css b/app/src/features/settings/components/DesktopSettings/DesktopSettings.css index c63eb7aed8..9b18a65276 100644 --- a/app/src/features/settings/components/DesktopSettings/DesktopSettings.css +++ b/app/src/features/settings/components/DesktopSettings/DesktopSettings.css @@ -13,13 +13,20 @@ } .desktop-settings-container { - display: flex; - justify-content: center; + /* Block (not flex) scroll container: the shared .settings-content ancestor is + height:100% + overflow:hidden, so scroll here instead of clipping. Using block + layout (centering the wrapper via margin:auto below) also preserves the wrapper's + bottom padding at the end of the scroll — a flex scroll container clips it. */ + height: 100%; + overflow-y: auto; } .desktop-settings-wrapper { + margin: 0 auto; /* center now that the container is block, not flex */ padding: 2rem; padding-top: 1rem; + /* extra breathing room so the last setting isn't flush against the bottom when scrolled */ + padding-bottom: 4rem; width: 100%; max-width: 1000px; } diff --git a/app/src/features/settings/components/DesktopSettings/DevScriptMode/devScriptMode.scss b/app/src/features/settings/components/DesktopSettings/DevScriptMode/devScriptMode.scss new file mode 100644 index 0000000000..2f7c004d45 --- /dev/null +++ b/app/src/features/settings/components/DesktopSettings/DevScriptMode/devScriptMode.scss @@ -0,0 +1,80 @@ +// RQ-2426: inline Script execution mode selector (Safe / Developer) in Desktop Settings. + +// This is the last section — drop the shared setting-item divider under it. +.dev-script-mode-setting.setting-item-container { + border-bottom: none; +} + +.dev-script-mode-selector { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 12px; + width: 100%; + + .dev-script-mode-option.ant-radio-wrapper { + display: flex; + align-items: flex-start; + width: 100%; + margin: 0; + padding: 12px 14px; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 8px; + white-space: normal; + transition: border-color 0.15s ease, background 0.15s ease; + + &:hover { + border-color: rgba(255, 255, 255, 0.28); + } + + &.ant-radio-wrapper-checked { + border-color: rgba(255, 255, 255, 0.45); + background: rgba(255, 255, 255, 0.04); + } + + .ant-radio { + margin-top: 2px; + } + + // antd puts the label content in the trailing span — stack title over description + > span:last-child { + display: flex; + flex-direction: column; + gap: 4px; + padding-inline-end: 0; + } + } + + .dev-script-mode-option__title { + font-weight: 600; + font-size: 13px; + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + } + + // Green "Default" chip, styled like the api-client "BETA" tag. + .dev-script-mode-option__tag { + font-size: 11px; + font-weight: 500; + padding: 0 6px; + border-radius: 8px; + color: #73d13d; + background: rgba(82, 196, 26, 0.15); + border: 1px solid rgba(82, 196, 26, 0.4); + } + + // Orange "(use only if you trust…)" caveat next to Developer Mode. + .dev-script-mode-option__caveat { + font-size: 12px; + font-weight: 500; + color: #fa8c16; + } + + .dev-script-mode-option__desc { + font-size: 12px; + line-height: 18px; + color: var(--requestly-color-text-subtle, #9b9b9b); + } +} diff --git a/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx b/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx new file mode 100644 index 0000000000..d87a136727 --- /dev/null +++ b/app/src/features/settings/components/DesktopSettings/DevScriptMode/index.tsx @@ -0,0 +1,112 @@ +import React, { useEffect, useState } from "react"; +import { Modal, Radio, RadioChangeEvent } from "antd"; +import { toast } from "utils/Toast"; +import "./devScriptMode.scss"; + +const GET_ACTION = "USER_PREFERENCE:GET_DEV_SCRIPT_MODE"; +const SET_ACTION = "USER_PREFERENCE:UPDATE_DEV_SCRIPT_MODE"; + +type ScriptMode = "safe" | "dev"; + +function storageAction(type: string, data?: any): Promise { + return (window as any)?.RQ?.DESKTOP?.SERVICES?.IPC?.invokeEventInMain("rq-storage:storage-action", { + type, + payload: data !== undefined ? { data } : {}, + }); +} + +/** + * RQ-2426: desktop-only selector for how Dynamic (JavaScript) request/response rules + * execute in the proxy. + * + * Safe Mode (default) → QuickJS-WASM sandbox, no host access. + * Developer Mode → legacy full host access (require/process/fs/child_process). + * + * Inline radio selector; the chosen mode is applied immediately, persisted in the + * desktop user-preference store and applied live on the running proxy (no restart). + */ +const DevScriptMode: React.FC = () => { + const [mode, setMode] = useState("safe"); + const [loading, setLoading] = useState(false); + + useEffect(() => { + storageAction(GET_ACTION) + ?.then((res: boolean) => setMode(res ? "dev" : "safe")) + .catch(() => {}); + }, []); + + const applyMode = async (next: ScriptMode) => { + const prev = mode; + setMode(next); // optimistic + setLoading(true); + try { + await storageAction(SET_ACTION, { devScriptMode: next === "dev" }); + toast.success( + next === "dev" + ? "Developer Mode enabled — rule scripts now run with full system access." + : "Safe Mode restored — rule scripts run inside the secure sandbox." + ); + } catch (err) { + setMode(prev); // revert on failure + toast.error("Failed to update script execution mode"); + } finally { + setLoading(false); + } + }; + + const onChange = (e: RadioChangeEvent) => { + const next = e.target.value as ScriptMode; + if (next === mode || loading) return; + + if (next === "dev") { + // Switching to full-access execution — warn before applying. The radio stays on + // the current value until the user confirms (Cancel is a no-op). + Modal.confirm({ + title: "Switch to Developer Mode?", + content: + "Rule scripts will run with FULL system access — they can read and write your files, execute system commands, and access sensitive information. A shared or imported rule could run arbitrary code on your machine. Only enable this for scripts you fully trust.", + okText: "Enable Developer Mode", + okButtonProps: { danger: true }, + cancelText: "Cancel", + width: 460, + onOk: () => applyMode("dev"), + }); + return; + } + + applyMode("safe"); + }; + + return ( +
+
Script execution mode
+

+ Dynamic (JavaScript) request/response rules run code in the desktop proxy. Choose the security level for that + execution. +

+ + + + + Safe Mode Default + + + Rule scripts run in a secure sandbox and cannot access your filesystem or execute system commands. + + + + + + Developer Mode{" "} + (use only if you trust the rule authors) + + + Rule scripts have access to the filesystem, can execute system commands, and access sensitive information. + + + +
+ ); +}; + +export default DevScriptMode; diff --git a/app/src/features/settings/components/DesktopSettings/index.jsx b/app/src/features/settings/components/DesktopSettings/index.jsx index 39ee5c0d79..28e638dc0e 100644 --- a/app/src/features/settings/components/DesktopSettings/index.jsx +++ b/app/src/features/settings/components/DesktopSettings/index.jsx @@ -20,6 +20,7 @@ import { RQButton } from "lib/design-system/components"; import "./DesktopSettings.css"; import LocalLogFile from "./LocalLogFile"; import InsecureCerts from "./InsecureCerts"; +import DevScriptMode from "./DevScriptMode"; export const DesktopSettings = () => { const appMode = useSelector(getAppMode); @@ -238,6 +239,7 @@ export const DesktopSettings = () => { ) : null} {isFeatureCompatible(FEATURES.ALLOW_INSECURE_SSL) && } + {isFeatureCompatible(FEATURES.DEVELOPER_SCRIPT_MODE) && } diff --git a/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/devScriptModeBadge.scss b/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/devScriptModeBadge.scss new file mode 100644 index 0000000000..a6234f3d96 --- /dev/null +++ b/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/devScriptModeBadge.scss @@ -0,0 +1,62 @@ +// RQ-2426: persistent "Developer Script Mode is ON" header indicator. +// Amber styling that reads on both light and dark themes (translucent amber). +.dev-script-mode-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + line-height: 18px; + white-space: nowrap; + cursor: pointer; + user-select: none; + color: #ffc069; + background: rgba(250, 140, 22, 0.15); + border: 1px solid rgba(250, 140, 22, 0.5); + + &__dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: #fa8c16; + flex: 0 0 auto; + } + + &:hover { + background: rgba(250, 140, 22, 0.25); + border-color: rgba(250, 140, 22, 0.7); + } +} + +// Hover popover content (rendered in a portal, so styled as top-level classes). +.dev-script-mode-badge__popover { + max-width: 260px; + + &-title { + font-weight: 600; + font-size: 13px; + margin-bottom: 4px; + color: #fa8c16; + } + + &-desc { + font-size: 12px; + line-height: 18px; + margin-bottom: 8px; + color: var(--requestly-color-text-subtle, #9b9b9b); + } + + &-link { + display: inline-block; + font-size: 12px; + font-weight: 500; + cursor: pointer; + color: var(--requestly-color-primary, #3b82f6); + + &:hover { + text-decoration: underline; + } + } +} diff --git a/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/index.tsx b/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/index.tsx new file mode 100644 index 0000000000..becd803388 --- /dev/null +++ b/app/src/layouts/DashboardLayout/MenuHeader/DevScriptModeBadge/index.tsx @@ -0,0 +1,96 @@ +import React, { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { Popover } from "antd"; +import { CONSTANTS as GLOBAL_CONSTANTS } from "@requestly/requestly-core"; +import { getAppMode } from "store/selectors"; +import { isFeatureCompatible } from "utils/CompatibilityUtils"; +import FEATURES from "config/constants/sub/features"; +import { redirectToDesktopSettings } from "utils/RedirectionUtils"; +import "./devScriptModeBadge.scss"; + +const GET_ACTION = "USER_PREFERENCE:GET_DEV_SCRIPT_MODE"; + +function getDevScriptModePref(): Promise | undefined { + return (window as any)?.RQ?.DESKTOP?.SERVICES?.IPC?.invokeEventInMain("rq-storage:storage-action", { + type: GET_ACTION, + payload: {}, + }); +} + +/** + * RQ-2426: persistent header indicator shown while Developer Script Mode is ON, so a + * user can't forget that rule scripts are executing with FULL system access (a + * shared/imported rule would run unsandboxed silently otherwise). + * + * Desktop-only; gated by the same feature flag as the toggle. Polls the preference + * so the badge reflects a live toggle without a reload. (Simple by design for now — + * could be made event-driven from the settings toggle later.) + */ +const DevScriptModeBadge: React.FC = () => { + const appMode = useSelector(getAppMode); + const navigate = useNavigate(); + const [enabled, setEnabled] = useState(false); + + const isDesktop = appMode === GLOBAL_CONSTANTS.APP_MODES.DESKTOP; + const isCompatible = isFeatureCompatible(FEATURES.DEVELOPER_SCRIPT_MODE); + + useEffect(() => { + if (!isDesktop || !isCompatible) return; + let active = true; + const check = () => { + getDevScriptModePref() + ?.then((res: boolean) => { + if (active) setEnabled(!!res); + }) + .catch(() => {}); + }; + check(); + const intervalId = setInterval(check, 4000); + return () => { + active = false; + clearInterval(intervalId); + }; + }, [isDesktop, isCompatible]); + + if (!isDesktop || !isCompatible || !enabled) { + return null; + } + + const popoverContent = ( +
+
Developer Mode is on
+
+ Dynamic (JavaScript) rule scripts run with full system access instead of the secure sandbox. Only keep this on + for scripts you fully trust. +
+ redirectToDesktopSettings(navigate, window.location.pathname, "dev_mode_badge_popover")} + > + Manage in Desktop Settings → + +
+ ); + + return ( + +
redirectToDesktopSettings(navigate, window.location.pathname, "dev_mode_badge")} + > + + Dev Mode +
+
+ ); +}; + +export default DevScriptModeBadge; diff --git a/app/src/layouts/DashboardLayout/MenuHeader/MenuHeader.tsx b/app/src/layouts/DashboardLayout/MenuHeader/MenuHeader.tsx index 8a1f36447c..30b260018d 100644 --- a/app/src/layouts/DashboardLayout/MenuHeader/MenuHeader.tsx +++ b/app/src/layouts/DashboardLayout/MenuHeader/MenuHeader.tsx @@ -17,6 +17,7 @@ import { getAppMode, getRequestBot } from "store/selectors"; import { CONSTANTS as GLOBAL_CONSTANTS } from "@requestly/requestly-core"; import { Col } from "antd"; import PremiumPlanBadge from "./PremiumPlanBadge/PremiumPlanBadge"; +import DevScriptModeBadge from "./DevScriptModeBadge"; import { getUserAuthDetails } from "store/slices/global/user/selectors"; import GitHubButton from "react-github-btn"; import "./menuHeader.scss"; @@ -61,6 +62,7 @@ export const MenuHeader = () => {
+ {!isSafariBrowser() && (appMode === GLOBAL_CONSTANTS.APP_MODES.DESKTOP || (appMode !== GLOBAL_CONSTANTS.APP_MODES.DESKTOP && diff --git a/documentation/docs.json b/documentation/docs.json index 9c32bc2c78..b71a4dc6ce 100644 --- a/documentation/docs.json +++ b/documentation/docs.json @@ -50,7 +50,8 @@ "interceptor/desktop-app/desktop-app-interception", "interceptor/desktop-app/network-table", "interceptor/desktop-app/saving-logs-to-local-file", - "interceptor/desktop-app/allow-insecure-ssl" + "interceptor/desktop-app/allow-insecure-ssl", + "interceptor/desktop-app/developer-script-mode" ] }, { @@ -199,9 +200,7 @@ }, { "group": "Other", - "pages": [ - "guides/other/claim-your-requestly-github-student-pack-benefit" - ] + "pages": ["guides/other/claim-your-requestly-github-student-pack-benefit"] } ] }, @@ -343,12 +342,7 @@ } }, "contextual": { - "options": [ - "copy", - "view", - "chatgpt", - "claude" - ] + "options": ["copy", "view", "chatgpt", "claude"] }, "integrations": { "ga4": { diff --git a/documentation/http-rules/advanced-usage/shared-state.mdx b/documentation/http-rules/advanced-usage/shared-state.mdx index 54c0b6be7d..aa2798ada3 100644 --- a/documentation/http-rules/advanced-usage/shared-state.mdx +++ b/documentation/http-rules/advanced-usage/shared-state.mdx @@ -15,6 +15,12 @@ Shared State allows developers to store and retrieve data within and across Modi Shared State was introduced in the Requestly Interceptor extension version `24.8.13` and Desktop App `1.7.1` . Make sure you are using the latest version. + + On the **desktop app**, programmatic rule scripts run in a secure sandbox by default (no + filesystem or system access). To use Node.js APIs like `require`, `fs`, or `process`, turn on + **Developer Script Mode**. + + ## Use Cases for Shared State 1. **Conditional Request/Response Modification**: Use shared state to modify requests or responses dynamically based on conditions stored in prior requests. For example, you can conditionally fail requests based on request counts. diff --git a/documentation/http-rules/rule-types/modify-request-body.mdx b/documentation/http-rules/rule-types/modify-request-body.mdx index c1edb97aac..da390c34bf 100644 --- a/documentation/http-rules/rule-types/modify-request-body.mdx +++ b/documentation/http-rules/rule-types/modify-request-body.mdx @@ -124,6 +124,13 @@ The Modify Request Body rule enables you to override or programmatically modify You can also use Dynamic Data to override GraphQL APIs by detecting the operation name in the request body. + + + On the **desktop app**, this JavaScript runs inside a secure sandbox by default — it has no + filesystem or system access. If your script needs Node.js APIs such as `require`, `fs`, or + `process`, enable **Developer Script Mode** + (off by default for safety). + diff --git a/documentation/http-rules/rule-types/modify-response-body.mdx b/documentation/http-rules/rule-types/modify-response-body.mdx index be9e4b9301..41f2b5cd13 100644 --- a/documentation/http-rules/rule-types/modify-response-body.mdx +++ b/documentation/http-rules/rule-types/modify-response-body.mdx @@ -109,6 +109,13 @@ Modify API Response Rules are useful in several scenarios: →Learn more about shared state here. + + On the **desktop app**, this JavaScript runs inside a secure sandbox by default — it has no + filesystem or system access. If your script needs Node.js APIs such as `require`, `fs`, or + `process`, enable **Developer Script Mode** + (off by default for safety). + + * diff --git a/documentation/images/developer-script-mode/.gitkeep b/documentation/images/developer-script-mode/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/documentation/interceptor/desktop-app/developer-script-mode.mdx b/documentation/interceptor/desktop-app/developer-script-mode.mdx new file mode 100644 index 0000000000..d25c3d134e --- /dev/null +++ b/documentation/interceptor/desktop-app/developer-script-mode.mdx @@ -0,0 +1,82 @@ +--- +title: "Developer Script Mode" +slug: "developer-script-mode" +path: "/http-interceptor/desktop-app/developer-script-mode" +visibility: "PUBLIC" +format: "MDX" +--- + +Requestly lets you write JavaScript to transform requests and responses — the +**Programmatic (JavaScript)** option in *Modify Request Body* and *Modify Response Body* +rules. On the desktop app this code runs inside the interception proxy. + +To keep you safe, that code runs in a **secure sandbox by default**. **Developer Script Mode** +is an opt-in setting that removes the sandbox and gives your rule scripts full access to your +system. + + + This setting applies only to the **desktop app**, and only to **"code" (Programmatic + JavaScript) rules**. Other rule types are unaffected. + + +## Why the sandbox exists + +Rule scripts travel between people — through shared lists, import/export, and team sync. A +script you import from someone else runs on **your** machine. If those scripts had unrestricted +access to Node.js, an imported rule could read your files, run commands, or make arbitrary +network calls without you realizing it. + +To prevent this, code rules run in an isolated JavaScript engine that has **no access to your +operating system**. + +## Safe mode (default) + +Your script runs in an isolated sandbox. It can do everything needed for typical +request/response manipulation, but it cannot touch the host machine. + +**Available** + +- Standard JavaScript, `console.*`, `args`, and `$sharedState` +- `fetch`, `XMLHttpRequest` +- `URL`, `URLSearchParams`, `TextEncoder` / `TextDecoder`, `Buffer`, `Blob`, `atob` / `btoa`, `structuredClone` +- `crypto` (`randomUUID`, `getRandomValues`, `subtle.digest`) and `require('crypto')` for hashing/HMAC + +**Blocked** + +- `require(...)` for anything other than `crypto` — e.g. `require('fs')`, `require('child_process')` +- `process`, the filesystem, spawning processes, and other Node.js / OS APIs + + + Scripts are also bounded by execution-time and memory limits so a runaway rule can't stall + interception. + + +## Developer Script Mode (opt‑in) + +When enabled, code rules run with **full system access** — the same capabilities a normal +Node.js script has (`require`, `process`, `fs`, `child_process`, …). Use it only when you +genuinely need it and only for scripts you fully trust. + + + In Developer Script Mode a shared or imported rule can run **arbitrary code on your machine** + with your privileges. Enable it only for code you have written or reviewed. + + +## Enabling / disabling + + + + Click the settings icon in the top‑right and go to **Desktop Settings**. You'll find the + **Developer script mode** toggle there (off by default). + + {/* Screenshot: the Desktop Settings screen with the "Developer script mode" row visible and the toggle OFF. Crop to the setting row + its description. */} + + + + Turn **Developer script mode** on. You'll be asked to confirm, since it grants full system + access. Turning it back off is immediate. + + {/* Screenshot: the confirmation dialog/popover that appears when enabling dev mode (the "Dev mode runs rule scripts with FULL system access…" prompt with Enable/Cancel). */} + + +