From dfeff522577e3be67e009ba05da36779557d055e Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 2 Apr 2026 23:14:20 +0000
Subject: [PATCH 1/7] feat: add Socket Firewall (sfw) support for npm installs
Integrates Socket Firewall (https://github.com/SocketDev/sfw-free) to
protect against supply chain attacks when installing npm dependencies
from Fiddle gists.
Changes:
- Add `sfw` as a dependency for wrapping npm/yarn commands
- Add `isUsingSocketFirewall` setting (enabled by default)
- Modify `addModules` to use sfw when enabled and available
- Add IPC handler `NPM_IS_SFW_INSTALLED` to check sfw availability
- Add settings toggle in Execution settings panel
- Add comprehensive tests for sfw integration
When enabled, Fiddle runs `sfw npm install` instead of `npm install`,
which scans packages during installation and blocks malicious ones.
Falls back to direct npm/yarn if sfw is not installed on the system.
https://claude.ai/code/session_01K6g5VZoNQRGLr4stRvHEVw
---
package.json | 1 +
src/ambient.d.ts | 3 +-
src/interfaces.ts | 2 +
src/ipc-events.ts | 2 +
src/main/npm.ts | 47 +++++-
src/preload/preload.ts | 7 +-
.../components/settings-execution.tsx | 43 ++++-
src/renderer/runner.ts | 9 +-
src/renderer/state.ts | 11 ++
tests/main/npm.spec.ts | 148 ++++++++++++++++++
yarn.lock | 73 +++++++--
11 files changed, 318 insertions(+), 28 deletions(-)
diff --git a/package.json b/package.json
index eaa99e8d67..791085eb46 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,7 @@
"react-mosaic-component": "^4.1.1",
"react-window": "^1.8.10",
"semver": "^7.3.4",
+ "sfw": "^2.0.4",
"shell-env": "^3.0.1",
"tmp": "0.2.5",
"tslib": "^2.6.0",
diff --git a/src/ambient.d.ts b/src/ambient.d.ts
index e2ba82b09a..7031286f2c 100644
--- a/src/ambient.d.ts
+++ b/src/ambient.d.ts
@@ -100,7 +100,7 @@ declare global {
listener: (types: string, version: string) => void,
): void;
addModules(
- { dir, packageManager }: PMOperationOptions,
+ { dir, packageManager, useSocketFirewall }: PMOperationOptions,
...names: Array
): Promise;
arch: string;
@@ -123,6 +123,7 @@ declare global {
packageManager: IPackageManager,
ignoreCache?: boolean,
): Promise;
+ getIsSfwInstalled(): Promise;
getProjectName(localPath?: string): Promise;
getTemplate(version: string): Promise;
getTemplateValues: (name: string) => Promise;
diff --git a/src/interfaces.ts b/src/interfaces.ts
index dd273da6d4..779757a015 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -213,6 +213,7 @@ export type IPackageManager = 'npm' | 'yarn';
export interface PMOperationOptions {
dir: string;
packageManager: IPackageManager;
+ useSocketFirewall?: boolean;
}
export interface GistRevision {
@@ -243,6 +244,7 @@ export enum GlobalSetting {
isEnablingElectronLogging = 'isEnablingElectronLogging',
isKeepingUserDataDirs = 'isKeepingUserDataDirs',
isPublishingGistAsRevision = 'isPublishingGistAsRevision',
+ isUsingSocketFirewall = 'isUsingSocketFirewall',
isUsingSystemTheme = 'isUsingSystemTheme',
knownVersion = 'known-electron-versions',
localVersion = 'local-electron-versions',
diff --git a/src/ipc-events.ts b/src/ipc-events.ts
index 1b90a15cfe..854a7c1aa8 100644
--- a/src/ipc-events.ts
+++ b/src/ipc-events.ts
@@ -45,6 +45,7 @@ export enum IpcEvents {
IS_DEV_MODE = 'IS_DEV_MODE',
NPM_ADD_MODULES = 'NPM_ADD_MODULES',
NPM_IS_PM_INSTALLED = 'NPM_IS_PM_INSTALLED',
+ NPM_IS_SFW_INSTALLED = 'NPM_IS_SFW_INSTALLED',
NPM_PACKAGE_RUN = 'NPM_PACKAGE_RUN',
FETCH_VERSIONS = 'FETCH_VERSIONS',
GET_LATEST_STABLE = 'GET_LATEST_STABLE',
@@ -99,6 +100,7 @@ export const ipcMainEvents = [
IpcEvents.IS_DEV_MODE,
IpcEvents.NPM_ADD_MODULES,
IpcEvents.NPM_IS_PM_INSTALLED,
+ IpcEvents.NPM_IS_SFW_INSTALLED,
IpcEvents.NPM_PACKAGE_RUN,
IpcEvents.FETCH_VERSIONS,
IpcEvents.GET_LATEST_STABLE,
diff --git a/src/main/npm.ts b/src/main/npm.ts
index 7003693e4c..8666af336f 100644
--- a/src/main/npm.ts
+++ b/src/main/npm.ts
@@ -9,6 +9,7 @@ import { IpcEvents } from '../ipc-events';
let isNpmInstalled: boolean | null = null;
let isYarnInstalled: boolean | null = null;
+let isSfwInstalled: boolean | null = null;
/**
* Checks if package manager is installed by checking if a binary
@@ -47,22 +48,53 @@ export async function getIsPackageManagerInstalled(
}
}
+/**
+ * Checks if sfw (Socket Firewall) is installed.
+ */
+export async function getIsSfwInstalled(
+ ignoreCache?: boolean,
+): Promise {
+ if (isSfwInstalled !== null && !ignoreCache) return isSfwInstalled;
+
+ const command = process.platform === 'win32' ? 'where.exe sfw' : 'which sfw';
+
+ try {
+ await exec(process.cwd(), command);
+ isSfwInstalled = true;
+ return true;
+ } catch (error) {
+ console.warn(`getIsSfwInstalled: "${command}" failed.`, error);
+ isSfwInstalled = false;
+ return false;
+ }
+}
+
/**
* Installs given modules to a given folder.
*/
export async function addModules(
- { dir, packageManager }: PMOperationOptions,
+ { dir, packageManager, useSocketFirewall }: PMOperationOptions,
...names: Array
): Promise {
- const cmd = packageManager === 'npm' ? 'npm' : 'yarn';
- const args =
+ const pm = packageManager === 'npm' ? 'npm' : 'yarn';
+ const pmArgs =
packageManager === 'npm'
? ['install', '-S', ...names]
: names.length > 0
? ['add', ...names]
: ['install'];
- return await execFile(dir, cmd, args);
+ // Use Socket Firewall if enabled and available
+ if (useSocketFirewall) {
+ const sfwAvailable = await getIsSfwInstalled();
+ if (sfwAvailable) {
+ // sfw wraps the package manager: sfw npm install ...
+ return await execFile(dir, 'sfw', [pm, ...pmArgs]);
+ }
+ console.warn('Socket Firewall requested but sfw is not installed');
+ }
+
+ return await execFile(dir, pm, pmArgs);
}
/**
@@ -84,9 +116,9 @@ export async function setupNpm() {
IpcEvents.NPM_ADD_MODULES,
(
_: IpcMainInvokeEvent,
- { dir, packageManager }: PMOperationOptions,
+ { dir, packageManager, useSocketFirewall }: PMOperationOptions,
...names: Array
- ) => addModules({ dir, packageManager }, ...names),
+ ) => addModules({ dir, packageManager, useSocketFirewall }, ...names),
);
ipcMainManager.handle(
IpcEvents.NPM_IS_PM_INSTALLED,
@@ -96,6 +128,9 @@ export async function setupNpm() {
ignoreCache?: boolean,
) => getIsPackageManagerInstalled(packageManager, ignoreCache),
);
+ ipcMainManager.handle(IpcEvents.NPM_IS_SFW_INSTALLED, () =>
+ getIsSfwInstalled(),
+ );
ipcMainManager.handle(
IpcEvents.NPM_PACKAGE_RUN,
(
diff --git a/src/preload/preload.ts b/src/preload/preload.ts
index 3684239b7f..69f5aeb108 100644
--- a/src/preload/preload.ts
+++ b/src/preload/preload.ts
@@ -76,12 +76,12 @@ export async function setupFiddleGlobal() {
}
},
addModules(
- { dir, packageManager }: PMOperationOptions,
+ { dir, packageManager, useSocketFirewall }: PMOperationOptions,
...names: Array
) {
return ipcRenderer.invoke(
IpcEvents.NPM_ADD_MODULES,
- { dir, packageManager },
+ { dir, packageManager, useSocketFirewall },
...names,
);
},
@@ -141,6 +141,9 @@ export async function setupFiddleGlobal() {
ignoreCache,
);
},
+ getIsSfwInstalled() {
+ return ipcRenderer.invoke(IpcEvents.NPM_IS_SFW_INSTALLED);
+ },
getNodeTypes(version: string) {
return ipcRenderer.invoke(IpcEvents.GET_NODE_TYPES, version);
},
diff --git a/src/renderer/components/settings-execution.tsx b/src/renderer/components/settings-execution.tsx
index 0ac5d7745e..536b4a46b2 100644
--- a/src/renderer/components/settings-execution.tsx
+++ b/src/renderer/components/settings-execution.tsx
@@ -57,6 +57,8 @@ export const ExecutionSettings = observer(
this.handleDeleteDataChange = this.handleDeleteDataChange.bind(this);
this.handleElectronLoggingChange =
this.handleElectronLoggingChange.bind(this);
+ this.handleSocketFirewallChange =
+ this.handleSocketFirewallChange.bind(this);
this.handleSettingsItemChange = this.handleSettingsItemChange.bind(this);
this.addNewSettingsItem = this.addNewSettingsItem.bind(this);
@@ -93,6 +95,16 @@ export const ExecutionSettings = observer(
this.props.appState.isEnablingElectronLogging = checked;
}
+ /**
+ * Handles a change on whether or not to use Socket Firewall for npm installs
+ */
+ public handleSocketFirewallChange(
+ event: React.FormEvent,
+ ) {
+ const { checked } = event.currentTarget;
+ this.props.appState.isUsingSocketFirewall = checked;
+ }
+
/**
* Handles a change in the execution flags or environment variables
* run with the Electron executable.
@@ -243,8 +255,11 @@ export const ExecutionSettings = observer(
}
public render() {
- const { isKeepingUserDataDirs, isEnablingElectronLogging } =
- this.props.appState;
+ const {
+ isKeepingUserDataDirs,
+ isEnablingElectronLogging,
+ isUsingSocketFirewall,
+ } = this.props.appState;
return (
@@ -348,6 +363,30 @@ export const ExecutionSettings = observer(
+
+
+
+
+
+ Socket Firewall
+ {' '}
+ protects against supply chain attacks by scanning packages
+ during installation. When enabled, Fiddle runs npm/yarn installs
+ through the sfw CLI, which blocks malicious dependencies before
+ they can execute. Requires npm install -g sfw to be
+ installed.
+
+
+
+
);
}
diff --git a/src/renderer/runner.ts b/src/renderer/runner.ts
index 684e181be9..6f5463da81 100644
--- a/src/renderer/runner.ts
+++ b/src/renderer/runner.ts
@@ -181,11 +181,12 @@ export class Runner {
const dir = await this.saveToTemp(options);
const packageManager = appState.packageManager;
+ const useSocketFirewall = appState.isUsingSocketFirewall;
if (!dir) return RunResult.INVALID;
try {
- await this.installModules({ dir, packageManager });
+ await this.installModules({ dir, packageManager, useSocketFirewall });
} catch (error: any) {
console.error('Runner: Could not install modules', error);
@@ -247,6 +248,7 @@ export class Runner {
pushOutput(`📦 ${strings[0]} current Fiddle...`);
const packageManager = this.appState.packageManager;
+ const useSocketFirewall = this.appState.isUsingSocketFirewall;
const pmInstalled =
await window.ElectronFiddle.getIsPackageManagerInstalled(packageManager);
if (!pmInstalled) {
@@ -264,7 +266,10 @@ export class Runner {
if (!dir) return false;
// Files are now saved to temp, let's install Forge and dependencies
- if (!(await this.packageInstall({ dir, packageManager }))) return false;
+ if (
+ !(await this.packageInstall({ dir, packageManager, useSocketFirewall }))
+ )
+ return false;
// Cool, let's run "package"
try {
diff --git a/src/renderer/state.ts b/src/renderer/state.ts
index 48d192c1ab..b1b6e415ee 100644
--- a/src/renderer/state.ts
+++ b/src/renderer/state.ts
@@ -99,6 +99,9 @@ export class AppState {
public isPublishingGistAsRevision = !!(
this.retrieve(GlobalSetting.isPublishingGistAsRevision) ?? true
);
+ public isUsingSocketFirewall = !!(
+ this.retrieve(GlobalSetting.isUsingSocketFirewall) ?? true
+ );
public executionFlags: Array =
(this.retrieve(GlobalSetting.executionFlags) as Array) === null
? []
@@ -243,6 +246,7 @@ export class AppState {
isKeepingUserDataDirs: observable,
isOnline: observable,
isPublishingGistAsRevision: observable,
+ isUsingSocketFirewall: observable,
isQuitting: observable,
isRunning: observable,
isSettingsShowing: observable,
@@ -424,6 +428,7 @@ export class AppState {
case GlobalSetting.isKeepingUserDataDirs:
case GlobalSetting.isPublishingGistAsRevision:
case GlobalSetting.isShowingGistHistory:
+ case GlobalSetting.isUsingSocketFirewall:
case GlobalSetting.isUsingSystemTheme:
case GlobalSetting.packageAuthor:
case GlobalSetting.packageManager:
@@ -499,6 +504,12 @@ export class AppState {
this.isPublishingGistAsRevision,
),
);
+ autorun(() =>
+ this.save(
+ GlobalSetting.isUsingSocketFirewall,
+ this.isUsingSocketFirewall,
+ ),
+ );
autorun(() =>
this.save(GlobalSetting.gitHubAvatarUrl, this.gitHubAvatarUrl),
);
diff --git a/tests/main/npm.spec.ts b/tests/main/npm.spec.ts
index 1ff0b43ae4..8a188156e8 100644
--- a/tests/main/npm.spec.ts
+++ b/tests/main/npm.spec.ts
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
addModules,
getIsPackageManagerInstalled,
+ getIsSfwInstalled,
packageRun,
} from '../../src/main/npm';
import { exec, execFile } from '../../src/main/utils/exec';
@@ -118,6 +119,59 @@ describe('npm', () => {
});
});
+ describe('getIsSfwInstalled()', () => {
+ beforeEach(() => {
+ vi.resetModules();
+ });
+
+ afterEach(() => resetPlatform());
+
+ it('returns true if sfw installed on darwin', async () => {
+ overridePlatform('darwin');
+
+ vi.mocked(exec).mockResolvedValueOnce('/usr/local/bin/sfw');
+
+ const result = await getIsSfwInstalled(true);
+
+ expect(result).toBe(true);
+ expect(exec).toBeCalledWith(expect.anything(), 'which sfw');
+ });
+
+ it('returns true if sfw installed on win32', async () => {
+ overridePlatform('win32');
+
+ vi.mocked(exec).mockResolvedValueOnce('C:\\Program Files\\sfw.exe');
+
+ const result = await getIsSfwInstalled(true);
+
+ expect(result).toBe(true);
+ expect(exec).toBeCalledWith(expect.anything(), 'where.exe sfw');
+ });
+
+ it('returns false if sfw not installed', async () => {
+ overridePlatform('darwin');
+
+ vi.mocked(exec).mockRejectedValueOnce(new Error('not found'));
+
+ const result = await getIsSfwInstalled(true);
+
+ expect(result).toBe(false);
+ expect(exec).toBeCalledWith(expect.anything(), 'which sfw');
+ });
+
+ it('uses the cache', async () => {
+ vi.mocked(exec).mockResolvedValueOnce('/usr/local/bin/sfw');
+
+ const one = await getIsSfwInstalled(true);
+ expect(one).toBe(true);
+ expect(exec).toHaveBeenCalledTimes(1);
+
+ const two = await getIsSfwInstalled();
+ expect(two).toBe(true);
+ expect(exec).toHaveBeenCalledTimes(1);
+ });
+ });
+
describe('addModules()', () => {
describe('npm', () => {
it('attempts to install a single module', async () => {
@@ -168,6 +222,100 @@ describe('npm', () => {
]);
});
});
+
+ describe('with socket firewall', () => {
+ afterEach(() => resetPlatform());
+
+ it('uses sfw when enabled and available for npm', async () => {
+ vi.resetModules();
+ const { addModules: addModulesFresh } = await import(
+ '../../src/main/npm'
+ );
+ overridePlatform('darwin');
+ vi.mocked(exec).mockResolvedValueOnce('/usr/local/bin/sfw');
+
+ await addModulesFresh(
+ {
+ dir: '/my/directory',
+ packageManager: 'npm',
+ useSocketFirewall: true,
+ },
+ 'lodash',
+ );
+
+ expect(execFile).toHaveBeenCalledWith('/my/directory', 'sfw', [
+ 'npm',
+ 'install',
+ '-S',
+ 'lodash',
+ ]);
+ });
+
+ it('uses sfw when enabled and available for yarn', async () => {
+ vi.resetModules();
+ const { addModules: addModulesFresh } = await import(
+ '../../src/main/npm'
+ );
+ overridePlatform('darwin');
+ vi.mocked(exec).mockResolvedValueOnce('/usr/local/bin/sfw');
+
+ await addModulesFresh(
+ {
+ dir: '/my/directory',
+ packageManager: 'yarn',
+ useSocketFirewall: true,
+ },
+ 'lodash',
+ );
+
+ expect(execFile).toHaveBeenCalledWith('/my/directory', 'sfw', [
+ 'yarn',
+ 'add',
+ 'lodash',
+ ]);
+ });
+
+ it('falls back to direct npm when sfw not available', async () => {
+ vi.resetModules();
+ const { addModules: addModulesFresh } = await import(
+ '../../src/main/npm'
+ );
+ overridePlatform('darwin');
+ vi.mocked(exec).mockRejectedValueOnce(new Error('not found'));
+
+ await addModulesFresh(
+ {
+ dir: '/my/directory',
+ packageManager: 'npm',
+ useSocketFirewall: true,
+ },
+ 'lodash',
+ );
+
+ expect(execFile).toHaveBeenCalledWith('/my/directory', 'npm', [
+ 'install',
+ '-S',
+ 'lodash',
+ ]);
+ });
+
+ it('does not use sfw when disabled', async () => {
+ addModules(
+ {
+ dir: '/my/directory',
+ packageManager: 'npm',
+ useSocketFirewall: false,
+ },
+ 'lodash',
+ );
+
+ expect(execFile).toHaveBeenCalledWith('/my/directory', 'npm', [
+ 'install',
+ '-S',
+ 'lodash',
+ ]);
+ });
+ });
});
describe('packageRun()', () => {
diff --git a/yarn.lock b/yarn.lock
index f26ca53a4a..3afd8c4ded 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6217,6 +6217,7 @@ __metadata:
react-window: "npm:^1.8.10"
resolve-url-loader: "npm:^5.0.0"
semver: "npm:^7.3.4"
+ sfw: "npm:^2.0.4"
shell-env: "npm:^3.0.1"
standard: "npm:^17.1.0"
stylelint: "npm:^15.10.1"
@@ -8012,7 +8013,7 @@ __metadata:
languageName: node
linkType: hard
-"graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
+"graceful-fs@npm:*, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
version: 4.2.11
resolution: "graceful-fs@npm:4.2.11"
checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2
@@ -12228,6 +12229,17 @@ __metadata:
languageName: node
linkType: hard
+"proper-lockfile@npm:*":
+ version: 4.1.2
+ resolution: "proper-lockfile@npm:4.1.2"
+ dependencies:
+ graceful-fs: "npm:^4.2.4"
+ retry: "npm:^0.12.0"
+ signal-exit: "npm:^3.0.2"
+ checksum: 10c0/2f265dbad15897a43110a02dae55105c04d356ec4ed560723dcb9f0d34bc4fb2f13f79bb930e7561be10278e2314db5aca2527d5d3dcbbdee5e6b331d1571f6d
+ languageName: node
+ linkType: hard
+
"property-information@npm:^7.0.0":
version: 7.1.0
resolution: "property-information@npm:7.1.0"
@@ -12969,6 +12981,13 @@ __metadata:
languageName: node
linkType: hard
+"retry@npm:*, retry@npm:^0.13.1":
+ version: 0.13.1
+ resolution: "retry@npm:0.13.1"
+ checksum: 10c0/9ae822ee19db2163497e074ea919780b1efa00431d197c7afdb950e42bf109196774b92a49fc9821f0b8b328a98eea6017410bfc5e8a0fc19c85c6d11adb3772
+ languageName: node
+ linkType: hard
+
"retry@npm:^0.12.0":
version: 0.12.0
resolution: "retry@npm:0.12.0"
@@ -12976,13 +12995,6 @@ __metadata:
languageName: node
linkType: hard
-"retry@npm:^0.13.1":
- version: 0.13.1
- resolution: "retry@npm:0.13.1"
- checksum: 10c0/9ae822ee19db2163497e074ea919780b1efa00431d197c7afdb950e42bf109196774b92a49fc9821f0b8b328a98eea6017410bfc5e8a0fc19c85c6d11adb3772
- languageName: node
- linkType: hard
-
"reusify@npm:^1.0.4":
version: 1.0.4
resolution: "reusify@npm:1.0.4"
@@ -13367,6 +13379,21 @@ __metadata:
languageName: node
linkType: hard
+"sfw@npm:^2.0.4":
+ version: 2.0.4
+ resolution: "sfw@npm:2.0.4"
+ dependencies:
+ graceful-fs: "npm:*"
+ proper-lockfile: "npm:*"
+ retry: "npm:*"
+ signal-exit: "npm:*"
+ yocto-spinner: "npm:*"
+ bin:
+ sfw: dist/sfw.mjs
+ checksum: 10c0/dbe35b8c7cac028ca2346759d9d491288d4d33fcaa7b28417fec347a09c2d685e4488ab22194df54805f7267163b4fa77a07fb2f44a01f57396ca14db15d6bff
+ languageName: node
+ linkType: hard
+
"shallow-clone@npm:^3.0.0":
version: 3.0.1
resolution: "shallow-clone@npm:3.0.1"
@@ -13452,6 +13479,13 @@ __metadata:
languageName: node
linkType: hard
+"signal-exit@npm:*, signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0":
+ version: 4.1.0
+ resolution: "signal-exit@npm:4.1.0"
+ checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83
+ languageName: node
+ linkType: hard
+
"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7":
version: 3.0.7
resolution: "signal-exit@npm:3.0.7"
@@ -13459,13 +13493,6 @@ __metadata:
languageName: node
linkType: hard
-"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0":
- version: 4.1.0
- resolution: "signal-exit@npm:4.1.0"
- checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83
- languageName: node
- linkType: hard
-
"simple-git@npm:^3.5.0":
version: 3.32.3
resolution: "simple-git@npm:3.32.3"
@@ -15773,9 +15800,25 @@ __metadata:
languageName: node
linkType: hard
+"yocto-spinner@npm:*":
+ version: 1.1.0
+ resolution: "yocto-spinner@npm:1.1.0"
+ dependencies:
+ yoctocolors: "npm:^2.1.1"
+ checksum: 10c0/4aa515543da3ccde81eb1037ff194954926f86f442dcf3e3bc99e4393185979157cae1d636888e8df2fa959fa39fd4d0c4856dc09a2f96285b2197b9ee265e13
+ languageName: node
+ linkType: hard
+
"yoctocolors-cjs@npm:^2.1.2":
version: 2.1.3
resolution: "yoctocolors-cjs@npm:2.1.3"
checksum: 10c0/584168ef98eb5d913473a4858dce128803c4a6cd87c0f09e954fa01126a59a33ab9e513b633ad9ab953786ed16efdd8c8700097a51635aafaeed3fef7712fa79
languageName: node
linkType: hard
+
+"yoctocolors@npm:^2.1.1":
+ version: 2.1.2
+ resolution: "yoctocolors@npm:2.1.2"
+ checksum: 10c0/b220f30f53ebc2167330c3adc86a3c7f158bcba0236f6c67e25644c3188e2571a6014ffc1321943bb619460259d3d27eb4c9cc58c2d884c1b195805883ec7066
+ languageName: node
+ linkType: hard
From 6287d0bc5b2ddbc0742e2d026fa4368208a78c59 Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 2 Apr 2026 23:18:32 +0000
Subject: [PATCH 2/7] docs: add THIRD_PARTY_NOTICES.md for sfw license
attribution
The sfw npm wrapper is MIT licensed, but the sfw-free binary it
downloads at runtime is under the PolyForm Shield License 1.0.0.
Include the full license text to satisfy the Notices provision.
https://claude.ai/code/session_01K6g5VZoNQRGLr4stRvHEVw
---
THIRD_PARTY_NOTICES.md | 155 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 155 insertions(+)
create mode 100644 THIRD_PARTY_NOTICES.md
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
new file mode 100644
index 0000000000..5a88365f7d
--- /dev/null
+++ b/THIRD_PARTY_NOTICES.md
@@ -0,0 +1,155 @@
+# Third-Party Notices
+
+Electron Fiddle incorporates third-party software components under
+various licenses. The following notices are provided in compliance
+with those licenses.
+
+---
+
+## Socket Firewall (sfw)
+
+- **npm package (`sfw`)**:
+- **License**: MIT
+- **Copyright**: Socket Inc.
+
+The `sfw` npm package is an MIT-licensed wrapper that downloads and
+manages the Socket Firewall Free binary.
+
+---
+
+## Socket Firewall Free (sfw-free)
+
+- **Source**:
+- **License**: PolyForm Shield License 1.0.0
+- **Copyright**: Socket Inc.
+
+The `sfw-free` binary, downloaded at runtime by the `sfw` npm
+package, is licensed under the PolyForm Shield License 1.0.0.
+The full license text is available at:
+
+
+### PolyForm Shield License 1.0.0
+
+> ## Acceptance
+>
+> In order to get any license under these terms, you must agree
+> to them as both strict obligations and conditions to all
+> your licenses.
+>
+> ## Copyright License
+>
+> The licensor grants you a copyright license for the
+> software to do everything you might do with the software
+> that would otherwise infringe the licensor's copyright
+> in it for any permitted purpose. However, you may
+> only distribute the software according to Distribution
+> License and make changes or new works based on the software
+> according to Changes and New Works License.
+>
+> ## Distribution License
+>
+> The licensor grants you an additional copyright license
+> to distribute copies of the software. Your license
+> to distribute covers distributing the software with
+> changes and new works permitted by Changes and New Works
+> License.
+>
+> ## Notices
+>
+> You must ensure that anyone who gets a copy of any part of
+> the software from you also gets a copy of these terms or the
+> URL for them above, as well as copies of any plain-text lines
+> beginning with `Required Notice:` that the licensor provided
+> with the software.
+>
+> ## Changes and New Works License
+>
+> The licensor grants you an additional copyright license to
+> make changes and new works based on the software for any
+> permitted purpose.
+>
+> ## Patent License
+>
+> The licensor grants you a patent license for the software that
+> covers patent claims the licensor can license, or becomes able
+> to license, that you would infringe by using the software.
+>
+> ## Noncompete
+>
+> Any purpose is a permitted purpose, except for providing any
+> product that competes with the software or any product the
+> licensor or any of its affiliates provides using the software.
+>
+> ## Competition
+>
+> Goods and services compete even when they provide functionality
+> through different kinds of interfaces or for different technical
+> platforms. Applications can compete with services, libraries
+> with plugins, frameworks with development tools, and so on,
+> even if they're written in different programming languages
+> or for different computer architectures. Goods and services
+> compete even when provided free of charge. If you market a
+> product as a practical substitute for the software or another
+> product, it definitely competes.
+>
+> ## New Products
+>
+> If you are using the software to provide a product that does
+> not compete, but the licensor or any of its affiliates brings
+> your product into competition by providing a new version of
+> the software or another product using the software, you may
+> continue using versions of the software available under these
+> terms beforehand to provide your competing product, but not
+> any later versions.
+>
+> ## Discontinued Products
+>
+> You may begin using the software to compete with a product
+> or service that the licensor or any of its affiliates has
+> stopped providing, unless the licensor includes a plain-text
+> line beginning with `Licensor Line of Business:` with the
+> software that mentions that line of business.
+>
+> ## Sales of Business
+>
+> If the licensor or any of its affiliates sells a line of
+> business developing the software or using the software
+> to provide a product, the buyer can also enforce
+> Noncompete for that product.
+>
+> ## Fair Use
+>
+> You may have "fair use" rights for the software under the
+> law. These terms do not limit them.
+>
+> ## No Other Rights
+>
+> These terms do not allow you to sublicense or transfer any of
+> your licenses to anyone else, or prevent the licensor from
+> granting licenses to anyone else. These terms do not imply
+> any other licenses.
+>
+> ## Patent Defense
+>
+> If you make any written claim that the software infringes or
+> contributes to infringement of any patent, your patent license
+> for the software granted under these terms ends immediately. If
+> your company makes such a claim, your patent license ends
+> immediately for work on behalf of your company.
+>
+> ## Violations
+>
+> The first time you are notified in writing that you have
+> violated any of these terms, or done anything with the software
+> not covered by your licenses, your licenses can nonetheless
+> continue if you come into full compliance with these terms,
+> and take practical steps to correct past violations, within
+> 32 days of receiving notice. Otherwise, all your licenses
+> end immediately.
+>
+> ## No Liability
+>
+> ***As far as the law allows, the software comes as is, without
+> any warranty or condition, and the licensor will not be liable
+> to you for any damages arising out of these terms or the use
+> or nature of the software, under any kind of legal claim.***
From f7fec8dd17ad56ae4d0eeaf169110905dca83162 Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 2 Apr 2026 23:23:47 +0000
Subject: [PATCH 3/7] feat: embed sfw script with the app instead of requiring
global install
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bundle the sfw CLI script (node_modules/sfw/dist/sfw.mjs) into the
webpack output via CopyPlugin so it ships with the packaged Electron
app. At runtime, resolve the embedded path and run it via the system
Node.js (`node sfw.mjs npm install ...`) rather than relying on a
globally installed `sfw` binary.
This means Socket Firewall works out of the box — users no longer
need to `npm install -g sfw`.
https://claude.ai/code/session_01K6g5VZoNQRGLr4stRvHEVw
---
src/main/npm.ts | 47 +++++-----
tests/main/npm.spec.ts | 123 ++++++++++++---------------
tools/webpack/webpack.main.config.ts | 1 +
3 files changed, 82 insertions(+), 89 deletions(-)
diff --git a/src/main/npm.ts b/src/main/npm.ts
index 8666af336f..719698497b 100644
--- a/src/main/npm.ts
+++ b/src/main/npm.ts
@@ -1,3 +1,4 @@
+import * as fs from 'node:fs';
import * as path from 'node:path';
import { IpcMainInvokeEvent, shell } from 'electron';
@@ -9,7 +10,7 @@ import { IpcEvents } from '../ipc-events';
let isNpmInstalled: boolean | null = null;
let isYarnInstalled: boolean | null = null;
-let isSfwInstalled: boolean | null = null;
+let sfwPath: string | null = null;
/**
* Checks if package manager is installed by checking if a binary
@@ -49,24 +50,28 @@ export async function getIsPackageManagerInstalled(
}
/**
- * Checks if sfw (Socket Firewall) is installed.
+ * Returns the path to the embedded sfw script, or null if not found.
+ * The sfw CLI is bundled with the app via webpack CopyPlugin.
*/
-export async function getIsSfwInstalled(
- ignoreCache?: boolean,
-): Promise {
- if (isSfwInstalled !== null && !ignoreCache) return isSfwInstalled;
+export function getSfwPath(): string | null {
+ if (sfwPath !== null) return sfwPath;
+
+ // Embedded sfw script copied by webpack CopyPlugin
+ const embeddedPath = path.resolve(__dirname, '../sfw/sfw.mjs');
+ if (fs.existsSync(embeddedPath)) {
+ sfwPath = embeddedPath;
+ return sfwPath;
+ }
- const command = process.platform === 'win32' ? 'where.exe sfw' : 'which sfw';
+ sfwPath = '';
+ return null;
+}
- try {
- await exec(process.cwd(), command);
- isSfwInstalled = true;
- return true;
- } catch (error) {
- console.warn(`getIsSfwInstalled: "${command}" failed.`, error);
- isSfwInstalled = false;
- return false;
- }
+/**
+ * Checks if sfw (Socket Firewall) is available (embedded with the app).
+ */
+export function getIsSfwInstalled(): boolean {
+ return getSfwPath() !== null;
}
/**
@@ -86,12 +91,12 @@ export async function addModules(
// Use Socket Firewall if enabled and available
if (useSocketFirewall) {
- const sfwAvailable = await getIsSfwInstalled();
- if (sfwAvailable) {
- // sfw wraps the package manager: sfw npm install ...
- return await execFile(dir, 'sfw', [pm, ...pmArgs]);
+ const sfwScript = getSfwPath();
+ if (sfwScript) {
+ // Run the embedded sfw script via system node: node sfw.mjs npm install ...
+ return await execFile(dir, 'node', [sfwScript, pm, ...pmArgs]);
}
- console.warn('Socket Firewall requested but sfw is not installed');
+ console.warn('Socket Firewall requested but sfw script was not found');
}
return await execFile(dir, pm, pmArgs);
diff --git a/tests/main/npm.spec.ts b/tests/main/npm.spec.ts
index 8a188156e8..8674363a1a 100644
--- a/tests/main/npm.spec.ts
+++ b/tests/main/npm.spec.ts
@@ -1,14 +1,21 @@
+import * as fs from 'node:fs';
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
addModules,
getIsPackageManagerInstalled,
getIsSfwInstalled,
+ getSfwPath,
packageRun,
-} from '../../src/main/npm';
+} from '../../src/main/npm.js';
import { exec, execFile } from '../../src/main/utils/exec';
import { overridePlatform, resetPlatform } from '../utils';
vi.mock('../../src/main/utils/exec');
+vi.mock('node:fs', async () => {
+ const actual = await vi.importActual('node:fs');
+ return { ...actual, existsSync: vi.fn() };
+});
describe('npm', () => {
describe('getIsPackageManagerInstalled()', () => {
@@ -119,56 +126,28 @@ describe('npm', () => {
});
});
- describe('getIsSfwInstalled()', () => {
- beforeEach(() => {
+ describe('getIsSfwInstalled() / getSfwPath()', () => {
+ it('returns true when the embedded sfw script exists', async () => {
vi.resetModules();
- });
-
- afterEach(() => resetPlatform());
-
- it('returns true if sfw installed on darwin', async () => {
- overridePlatform('darwin');
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
- vi.mocked(exec).mockResolvedValueOnce('/usr/local/bin/sfw');
+ const { getIsSfwInstalled: fresh, getSfwPath: freshPath } = await import(
+ '../../src/main/npm.js'
+ );
- const result = await getIsSfwInstalled(true);
-
- expect(result).toBe(true);
- expect(exec).toBeCalledWith(expect.anything(), 'which sfw');
+ expect(fresh()).toBe(true);
+ expect(freshPath()).toMatch(/sfw\.mjs$/);
});
- it('returns true if sfw installed on win32', async () => {
- overridePlatform('win32');
-
- vi.mocked(exec).mockResolvedValueOnce('C:\\Program Files\\sfw.exe');
-
- const result = await getIsSfwInstalled(true);
-
- expect(result).toBe(true);
- expect(exec).toBeCalledWith(expect.anything(), 'where.exe sfw');
- });
-
- it('returns false if sfw not installed', async () => {
- overridePlatform('darwin');
-
- vi.mocked(exec).mockRejectedValueOnce(new Error('not found'));
-
- const result = await getIsSfwInstalled(true);
-
- expect(result).toBe(false);
- expect(exec).toBeCalledWith(expect.anything(), 'which sfw');
- });
-
- it('uses the cache', async () => {
- vi.mocked(exec).mockResolvedValueOnce('/usr/local/bin/sfw');
+ it('returns false when the embedded sfw script does not exist', async () => {
+ vi.resetModules();
+ vi.mocked(fs.existsSync).mockReturnValueOnce(false);
- const one = await getIsSfwInstalled(true);
- expect(one).toBe(true);
- expect(exec).toHaveBeenCalledTimes(1);
+ const { getIsSfwInstalled: fresh } = await import(
+ '../../src/main/npm.js'
+ );
- const two = await getIsSfwInstalled();
- expect(two).toBe(true);
- expect(exec).toHaveBeenCalledTimes(1);
+ expect(fresh()).toBe(false);
});
});
@@ -224,15 +203,13 @@ describe('npm', () => {
});
describe('with socket firewall', () => {
- afterEach(() => resetPlatform());
-
- it('uses sfw when enabled and available for npm', async () => {
+ it('uses sfw when enabled and embedded script exists', async () => {
vi.resetModules();
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
+
const { addModules: addModulesFresh } = await import(
- '../../src/main/npm'
+ '../../src/main/npm.js'
);
- overridePlatform('darwin');
- vi.mocked(exec).mockResolvedValueOnce('/usr/local/bin/sfw');
await addModulesFresh(
{
@@ -243,21 +220,26 @@ describe('npm', () => {
'lodash',
);
- expect(execFile).toHaveBeenCalledWith('/my/directory', 'sfw', [
- 'npm',
- 'install',
- '-S',
- 'lodash',
- ]);
+ expect(execFile).toHaveBeenCalledWith(
+ '/my/directory',
+ 'node',
+ expect.arrayContaining([
+ expect.stringMatching(/sfw\.mjs$/),
+ 'npm',
+ 'install',
+ '-S',
+ 'lodash',
+ ]),
+ );
});
it('uses sfw when enabled and available for yarn', async () => {
vi.resetModules();
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
+
const { addModules: addModulesFresh } = await import(
- '../../src/main/npm'
+ '../../src/main/npm.js'
);
- overridePlatform('darwin');
- vi.mocked(exec).mockResolvedValueOnce('/usr/local/bin/sfw');
await addModulesFresh(
{
@@ -268,20 +250,25 @@ describe('npm', () => {
'lodash',
);
- expect(execFile).toHaveBeenCalledWith('/my/directory', 'sfw', [
- 'yarn',
- 'add',
- 'lodash',
- ]);
+ expect(execFile).toHaveBeenCalledWith(
+ '/my/directory',
+ 'node',
+ expect.arrayContaining([
+ expect.stringMatching(/sfw\.mjs$/),
+ 'yarn',
+ 'add',
+ 'lodash',
+ ]),
+ );
});
- it('falls back to direct npm when sfw not available', async () => {
+ it('falls back to direct npm when sfw script not found', async () => {
vi.resetModules();
+ vi.mocked(fs.existsSync).mockReturnValueOnce(false);
+
const { addModules: addModulesFresh } = await import(
- '../../src/main/npm'
+ '../../src/main/npm.js'
);
- overridePlatform('darwin');
- vi.mocked(exec).mockRejectedValueOnce(new Error('not found'));
await addModulesFresh(
{
diff --git a/tools/webpack/webpack.main.config.ts b/tools/webpack/webpack.main.config.ts
index 2e75785b70..486b6ca1ee 100644
--- a/tools/webpack/webpack.main.config.ts
+++ b/tools/webpack/webpack.main.config.ts
@@ -32,6 +32,7 @@ export const mainConfig: Configuration = {
},
{ from: 'static/show-me', to: '../static/show-me' },
{ from: 'assets/icons/fiddle.png', to: '../assets/icons/fiddle.png' },
+ { from: 'node_modules/sfw/dist/sfw.mjs', to: '../sfw/sfw.mjs' },
],
}),
],
From e03c60bfa582cca43a828aa2b7bd57c1bc5b2e92 Mon Sep 17 00:00:00 2001
From: Samuel Attard
Date: Thu, 2 Apr 2026 16:35:23 -0700
Subject: [PATCH 4/7] fix: unpack embedded sfw from asar and ship its
package.json
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
sfw.mjs reads ../package.json at runtime to populate its version string,
so the bundled layout must mirror node_modules/sfw/ (dist/sfw.mjs plus
a sibling package.json) — copying just sfw.mjs leaves it looking for
package.json one directory too high and crashes with ENOENT.
In packaged builds, sfw.mjs also has to live outside the asar archive
because system Node can't read asar. Mark .webpack/sfw/** as unpacked
and translate app.asar -> app.asar.unpacked when resolving the path.
The glob needs the explicit .webpack segment because minimatch's
globstar skips dot-prefixed directories by default.
Also update the settings copy now that sfw ships with the app.
---
forge.config.ts | 5 ++++-
src/main/npm.ts | 13 +++++++++++--
src/renderer/components/settings-execution.tsx | 4 ++--
tools/webpack/webpack.main.config.ts | 3 ++-
4 files changed, 19 insertions(+), 6 deletions(-)
diff --git a/forge.config.ts b/forge.config.ts
index 591a8e2822..1970165a5b 100644
--- a/forge.config.ts
+++ b/forge.config.ts
@@ -70,7 +70,10 @@ const config: ForgeConfig = {
packagerConfig: {
name: 'Electron Fiddle',
executableName: 'electron-fiddle',
- asar: true,
+ // Unpack the embedded sfw script so system Node can spawn it — it can't
+ // be executed from inside an asar archive. The `.webpack` segment must
+ // be explicit because minimatch globstar skips dot-prefixed directories.
+ asar: { unpack: '**/.webpack/sfw/**' },
icon: path.resolve(__dirname, 'assets', 'icons', 'fiddle'),
appBundleId: 'com.electron.fiddle',
usageDescription: {
diff --git a/src/main/npm.ts b/src/main/npm.ts
index 719698497b..bd158e9843 100644
--- a/src/main/npm.ts
+++ b/src/main/npm.ts
@@ -56,8 +56,17 @@ export async function getIsPackageManagerInstalled(
export function getSfwPath(): string | null {
if (sfwPath !== null) return sfwPath;
- // Embedded sfw script copied by webpack CopyPlugin
- const embeddedPath = path.resolve(__dirname, '../sfw/sfw.mjs');
+ // Embedded sfw script copied by webpack CopyPlugin.
+ // Mirrors the original node_modules/sfw/ layout (dist/sfw.mjs + package.json)
+ // because sfw.mjs reads "../package.json" at runtime for its version.
+ // In a packaged app the sfw directory is asar-unpacked (see forge.config.ts)
+ // so system Node can read it — translate the virtual asar path accordingly.
+ const embeddedPath = path
+ .resolve(__dirname, '../sfw/dist/sfw.mjs')
+ .replace(
+ `${path.sep}app.asar${path.sep}`,
+ `${path.sep}app.asar.unpacked${path.sep}`,
+ );
if (fs.existsSync(embeddedPath)) {
sfwPath = embeddedPath;
return sfwPath;
diff --git a/src/renderer/components/settings-execution.tsx b/src/renderer/components/settings-execution.tsx
index 536b4a46b2..111106793f 100644
--- a/src/renderer/components/settings-execution.tsx
+++ b/src/renderer/components/settings-execution.tsx
@@ -377,8 +377,8 @@ export const ExecutionSettings = observer(
protects against supply chain attacks by scanning packages
during installation. When enabled, Fiddle runs npm/yarn installs
through the sfw CLI, which blocks malicious dependencies before
- they can execute. Requires npm install -g sfw to be
- installed.
+ they can execute. The sfw CLI is bundled with Fiddle, so no
+ extra install is required.
Date: Tue, 7 Apr 2026 03:39:59 +0000
Subject: [PATCH 5/7] fix: remove outdated instruction to install sfw globally
The sfw CLI is now embedded with the app, so users no longer need
to run `npm install -g sfw`.
https://claude.ai/code/session_01K6g5VZoNQRGLr4stRvHEVw
---
src/renderer/components/settings-execution.tsx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/renderer/components/settings-execution.tsx b/src/renderer/components/settings-execution.tsx
index 111106793f..7dfe47203b 100644
--- a/src/renderer/components/settings-execution.tsx
+++ b/src/renderer/components/settings-execution.tsx
@@ -377,8 +377,7 @@ export const ExecutionSettings = observer(
protects against supply chain attacks by scanning packages
during installation. When enabled, Fiddle runs npm/yarn installs
through the sfw CLI, which blocks malicious dependencies before
- they can execute. The sfw CLI is bundled with Fiddle, so no
- extra install is required.
+ they can execute.
Date: Tue, 7 Apr 2026 03:43:53 +0000
Subject: [PATCH 6/7] chore: remove unused getIsSfwInstalled IPC and simplify
sfw path
- Remove `getIsSfwInstalled()` function and `NPM_IS_SFW_INSTALLED` IPC
event (never called from renderer)
- Remove null-checking/fallback logic in `getSfwPath()` - the sfw script
is embedded via CopyPlugin, so if it's missing that's a build bug
- Remove fs.existsSync call and caching - just compute the path directly
- Simplify tests accordingly
https://claude.ai/code/session_01K6g5VZoNQRGLr4stRvHEVw
---
src/ambient.d.ts | 1 -
src/ipc-events.ts | 2 --
src/main/npm.ts | 46 ++++++------------------
src/preload/preload.ts | 3 --
tests/main/npm.spec.ts | 80 +++++-------------------------------------
5 files changed, 18 insertions(+), 114 deletions(-)
diff --git a/src/ambient.d.ts b/src/ambient.d.ts
index 7031286f2c..c921876841 100644
--- a/src/ambient.d.ts
+++ b/src/ambient.d.ts
@@ -123,7 +123,6 @@ declare global {
packageManager: IPackageManager,
ignoreCache?: boolean,
): Promise;
- getIsSfwInstalled(): Promise;
getProjectName(localPath?: string): Promise;
getTemplate(version: string): Promise;
getTemplateValues: (name: string) => Promise;
diff --git a/src/ipc-events.ts b/src/ipc-events.ts
index 854a7c1aa8..1b90a15cfe 100644
--- a/src/ipc-events.ts
+++ b/src/ipc-events.ts
@@ -45,7 +45,6 @@ export enum IpcEvents {
IS_DEV_MODE = 'IS_DEV_MODE',
NPM_ADD_MODULES = 'NPM_ADD_MODULES',
NPM_IS_PM_INSTALLED = 'NPM_IS_PM_INSTALLED',
- NPM_IS_SFW_INSTALLED = 'NPM_IS_SFW_INSTALLED',
NPM_PACKAGE_RUN = 'NPM_PACKAGE_RUN',
FETCH_VERSIONS = 'FETCH_VERSIONS',
GET_LATEST_STABLE = 'GET_LATEST_STABLE',
@@ -100,7 +99,6 @@ export const ipcMainEvents = [
IpcEvents.IS_DEV_MODE,
IpcEvents.NPM_ADD_MODULES,
IpcEvents.NPM_IS_PM_INSTALLED,
- IpcEvents.NPM_IS_SFW_INSTALLED,
IpcEvents.NPM_PACKAGE_RUN,
IpcEvents.FETCH_VERSIONS,
IpcEvents.GET_LATEST_STABLE,
diff --git a/src/main/npm.ts b/src/main/npm.ts
index bd158e9843..7e0886772a 100644
--- a/src/main/npm.ts
+++ b/src/main/npm.ts
@@ -1,4 +1,3 @@
-import * as fs from 'node:fs';
import * as path from 'node:path';
import { IpcMainInvokeEvent, shell } from 'electron';
@@ -10,7 +9,6 @@ import { IpcEvents } from '../ipc-events';
let isNpmInstalled: boolean | null = null;
let isYarnInstalled: boolean | null = null;
-let sfwPath: string | null = null;
/**
* Checks if package manager is installed by checking if a binary
@@ -50,37 +48,20 @@ export async function getIsPackageManagerInstalled(
}
/**
- * Returns the path to the embedded sfw script, or null if not found.
+ * Returns the path to the embedded sfw script.
* The sfw CLI is bundled with the app via webpack CopyPlugin.
+ * Mirrors the original node_modules/sfw/ layout (dist/sfw.mjs + package.json)
+ * because sfw.mjs reads "../package.json" at runtime for its version.
+ * In a packaged app the sfw directory is asar-unpacked (see forge.config.ts)
+ * so system Node can read it — translate the virtual asar path accordingly.
*/
-export function getSfwPath(): string | null {
- if (sfwPath !== null) return sfwPath;
-
- // Embedded sfw script copied by webpack CopyPlugin.
- // Mirrors the original node_modules/sfw/ layout (dist/sfw.mjs + package.json)
- // because sfw.mjs reads "../package.json" at runtime for its version.
- // In a packaged app the sfw directory is asar-unpacked (see forge.config.ts)
- // so system Node can read it — translate the virtual asar path accordingly.
- const embeddedPath = path
+export function getSfwPath(): string {
+ return path
.resolve(__dirname, '../sfw/dist/sfw.mjs')
.replace(
`${path.sep}app.asar${path.sep}`,
`${path.sep}app.asar.unpacked${path.sep}`,
);
- if (fs.existsSync(embeddedPath)) {
- sfwPath = embeddedPath;
- return sfwPath;
- }
-
- sfwPath = '';
- return null;
-}
-
-/**
- * Checks if sfw (Socket Firewall) is available (embedded with the app).
- */
-export function getIsSfwInstalled(): boolean {
- return getSfwPath() !== null;
}
/**
@@ -98,14 +79,10 @@ export async function addModules(
? ['add', ...names]
: ['install'];
- // Use Socket Firewall if enabled and available
+ // Use Socket Firewall if enabled
if (useSocketFirewall) {
- const sfwScript = getSfwPath();
- if (sfwScript) {
- // Run the embedded sfw script via system node: node sfw.mjs npm install ...
- return await execFile(dir, 'node', [sfwScript, pm, ...pmArgs]);
- }
- console.warn('Socket Firewall requested but sfw script was not found');
+ // Run the embedded sfw script via system node: node sfw.mjs npm install ...
+ return await execFile(dir, 'node', [getSfwPath(), pm, ...pmArgs]);
}
return await execFile(dir, pm, pmArgs);
@@ -142,9 +119,6 @@ export async function setupNpm() {
ignoreCache?: boolean,
) => getIsPackageManagerInstalled(packageManager, ignoreCache),
);
- ipcMainManager.handle(IpcEvents.NPM_IS_SFW_INSTALLED, () =>
- getIsSfwInstalled(),
- );
ipcMainManager.handle(
IpcEvents.NPM_PACKAGE_RUN,
(
diff --git a/src/preload/preload.ts b/src/preload/preload.ts
index 69f5aeb108..1ef770b30a 100644
--- a/src/preload/preload.ts
+++ b/src/preload/preload.ts
@@ -141,9 +141,6 @@ export async function setupFiddleGlobal() {
ignoreCache,
);
},
- getIsSfwInstalled() {
- return ipcRenderer.invoke(IpcEvents.NPM_IS_SFW_INSTALLED);
- },
getNodeTypes(version: string) {
return ipcRenderer.invoke(IpcEvents.GET_NODE_TYPES, version);
},
diff --git a/tests/main/npm.spec.ts b/tests/main/npm.spec.ts
index 8674363a1a..41857b3805 100644
--- a/tests/main/npm.spec.ts
+++ b/tests/main/npm.spec.ts
@@ -1,21 +1,14 @@
-import * as fs from 'node:fs';
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
addModules,
getIsPackageManagerInstalled,
- getIsSfwInstalled,
getSfwPath,
packageRun,
} from '../../src/main/npm.js';
import { exec, execFile } from '../../src/main/utils/exec';
import { overridePlatform, resetPlatform } from '../utils';
vi.mock('../../src/main/utils/exec');
-vi.mock('node:fs', async () => {
- const actual = await vi.importActual('node:fs');
- return { ...actual, existsSync: vi.fn() };
-});
describe('npm', () => {
describe('getIsPackageManagerInstalled()', () => {
@@ -126,28 +119,9 @@ describe('npm', () => {
});
});
- describe('getIsSfwInstalled() / getSfwPath()', () => {
- it('returns true when the embedded sfw script exists', async () => {
- vi.resetModules();
- vi.mocked(fs.existsSync).mockReturnValueOnce(true);
-
- const { getIsSfwInstalled: fresh, getSfwPath: freshPath } = await import(
- '../../src/main/npm.js'
- );
-
- expect(fresh()).toBe(true);
- expect(freshPath()).toMatch(/sfw\.mjs$/);
- });
-
- it('returns false when the embedded sfw script does not exist', async () => {
- vi.resetModules();
- vi.mocked(fs.existsSync).mockReturnValueOnce(false);
-
- const { getIsSfwInstalled: fresh } = await import(
- '../../src/main/npm.js'
- );
-
- expect(fresh()).toBe(false);
+ describe('getSfwPath()', () => {
+ it('returns the path to the embedded sfw script', () => {
+ expect(getSfwPath()).toMatch(/sfw\.mjs$/);
});
});
@@ -203,15 +177,8 @@ describe('npm', () => {
});
describe('with socket firewall', () => {
- it('uses sfw when enabled and embedded script exists', async () => {
- vi.resetModules();
- vi.mocked(fs.existsSync).mockReturnValueOnce(true);
-
- const { addModules: addModulesFresh } = await import(
- '../../src/main/npm.js'
- );
-
- await addModulesFresh(
+ it('uses sfw when enabled for npm', async () => {
+ await addModules(
{
dir: '/my/directory',
packageManager: 'npm',
@@ -233,15 +200,8 @@ describe('npm', () => {
);
});
- it('uses sfw when enabled and available for yarn', async () => {
- vi.resetModules();
- vi.mocked(fs.existsSync).mockReturnValueOnce(true);
-
- const { addModules: addModulesFresh } = await import(
- '../../src/main/npm.js'
- );
-
- await addModulesFresh(
+ it('uses sfw when enabled for yarn', async () => {
+ await addModules(
{
dir: '/my/directory',
packageManager: 'yarn',
@@ -262,32 +222,8 @@ describe('npm', () => {
);
});
- it('falls back to direct npm when sfw script not found', async () => {
- vi.resetModules();
- vi.mocked(fs.existsSync).mockReturnValueOnce(false);
-
- const { addModules: addModulesFresh } = await import(
- '../../src/main/npm.js'
- );
-
- await addModulesFresh(
- {
- dir: '/my/directory',
- packageManager: 'npm',
- useSocketFirewall: true,
- },
- 'lodash',
- );
-
- expect(execFile).toHaveBeenCalledWith('/my/directory', 'npm', [
- 'install',
- '-S',
- 'lodash',
- ]);
- });
-
it('does not use sfw when disabled', async () => {
- addModules(
+ await addModules(
{
dir: '/my/directory',
packageManager: 'npm',
From 5609db12786f6d3d8bb62037b206ebf101b27219 Mon Sep 17 00:00:00 2001
From: David Sanders
Date: Tue, 7 Apr 2026 21:09:38 -0700
Subject: [PATCH 7/7] chore: remove unnecessary change
---
tests/main/npm.spec.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/main/npm.spec.ts b/tests/main/npm.spec.ts
index 41857b3805..94c9a1204a 100644
--- a/tests/main/npm.spec.ts
+++ b/tests/main/npm.spec.ts
@@ -5,7 +5,7 @@ import {
getIsPackageManagerInstalled,
getSfwPath,
packageRun,
-} from '../../src/main/npm.js';
+} from '../../src/main/npm';
import { exec, execFile } from '../../src/main/utils/exec';
import { overridePlatform, resetPlatform } from '../utils';
vi.mock('../../src/main/utils/exec');