From 879b37e164d0a264a9f129a57adad29232685684 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 12:47:57 +0100 Subject: [PATCH 01/46] Add ultimate installer for Windows --- packages/safe-chain/bin/safe-chain.js | 32 +++--- .../src/installation/installUltimate.js | 101 ++++++++++++++++++ 2 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 packages/safe-chain/src/installation/installUltimate.js diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 841ccee..e33ad9f 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -16,6 +16,7 @@ import path from "path"; import { fileURLToPath } from "url"; import fs from "fs"; import { knownAikidoTools } from "../src/shell-integration/helpers.js"; +import { installUltimate } from "../src/installation/installUltimate.js"; /** @type {string} */ // This checks the current file's dirname in a way that's compatible with: @@ -62,6 +63,8 @@ if (tool) { process.exit(0); } else if (command === "setup") { setup(); +} else if (command === "--ultimate") { + installUltimate(); } else if (command === "teardown") { teardownDirectories(); teardown(); @@ -82,36 +85,41 @@ if (tool) { function writeHelp() { ui.writeInformation( - chalk.bold("Usage: ") + chalk.cyan("safe-chain ") + chalk.bold("Usage: ") + chalk.cyan("safe-chain "), ); ui.emptyLine(); ui.writeInformation( `Available commands: ${chalk.cyan("setup")}, ${chalk.cyan( - "teardown" + "teardown", )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan( - "--version" - )}` + "--version", + )}`, ); ui.emptyLine(); ui.writeInformation( `- ${chalk.cyan( - "safe-chain setup" - )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.` + "safe-chain setup", + )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.`, ); ui.writeInformation( `- ${chalk.cyan( - "safe-chain teardown" - )}: This will remove safe-chain aliases from your shell configuration.` + "safe-chain --ultimate", + )}: This installs the ultimate version of safe-chain, enabling protection for more eco-systems (vscode).`, ); ui.writeInformation( `- ${chalk.cyan( - "safe-chain setup-ci" - )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.` + "safe-chain teardown", + )}: This will remove safe-chain aliases from your shell configuration.`, + ); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain setup-ci", + )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`, ); ui.writeInformation( `- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan( - "-v" - )}): Display the current version of safe-chain.` + "-v", + )}): Display the current version of safe-chain.`, ); ui.emptyLine(); } diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js new file mode 100644 index 0000000..e1323db --- /dev/null +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -0,0 +1,101 @@ +import { platform, arch, tmpdir } from "os"; +import { createWriteStream, unlinkSync } from "fs"; +import { join } from "path"; +import { execSync } from "child_process"; +import { pipeline } from "stream/promises"; +import fetch from "make-fetch-happen"; +import { ui } from "../environment/userInteraction.js"; + +const ULTIMATE_VERSION = "v0.2.0"; + +export function installUltimate() { + const operatingSystem = platform(); + + if (operatingSystem === "win32") { + installOnWindows(); + } else { + ui.writeInformation( + `${operatingSystem} is not supported yet by safe-chain's ultimate version.`, + ); + } +} + +async function installOnWindows() { + if (!isRunningAsAdmin()) { + ui.writeError("Administrator privileges required."); + ui.writeInformation( + "Please run this command in an elevated terminal (Run as Administrator)." + ); + return; + } + + const architecture = getWindowsArchitecture(); + const downloadUrl = buildDownloadUrl(architecture); + const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); + + ui.writeInformation(`Downloading SafeChain Agent ${ULTIMATE_VERSION} for ${architecture}...`); + ui.writeVerbose(`Download URL: ${downloadUrl}`); + ui.writeVerbose(`Destination: ${msiPath}`); + await downloadFile(downloadUrl, msiPath); + + ui.writeInformation("Installing SafeChain Agent..."); + ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); + runMsiInstaller(msiPath); + + ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); + cleanup(msiPath); + ui.writeInformation("SafeChain Agent installed successfully!"); +} + +function isRunningAsAdmin() { + try { + execSync("net session", { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +function getWindowsArchitecture() { + const nodeArch = arch(); + if (nodeArch === "x64") return "amd64"; + if (nodeArch === "arm64") return "arm64"; + throw new Error(`Unsupported architecture: ${nodeArch}`); +} + +/** + * @param {string} architecture + */ +function buildDownloadUrl(architecture) { + return `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-windows-${architecture}.msi`; +} + +/** + * @param {string} url + * @param {string} destPath + */ +async function downloadFile(url, destPath) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Download failed: ${response.statusText}`); + } + await pipeline(response.body, createWriteStream(destPath)); +} + +/** + * @param {string} msiPath + */ +function runMsiInstaller(msiPath) { + execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); +} + +/** + * @param {string} msiPath + */ +function cleanup(msiPath) { + try { + unlinkSync(msiPath); + } catch { + // Ignore cleanup errors + } +} From 2c0245b020ecbed7ee4c28212d7653970458e288 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 13:28:16 +0100 Subject: [PATCH 02/46] Start and stop safe-chain agent's Windows service. --- .../src/installation/installUltimate.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index e1323db..a638407 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -38,13 +38,18 @@ async function installOnWindows() { ui.writeVerbose(`Destination: ${msiPath}`); await downloadFile(downloadUrl, msiPath); + stopServiceIfRunning(); + ui.writeInformation("Installing SafeChain Agent..."); ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); runMsiInstaller(msiPath); + ui.writeInformation("Starting SafeChain Agent service..."); + startService(); + ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); cleanup(msiPath); - ui.writeInformation("SafeChain Agent installed successfully!"); + ui.writeInformation("SafeChain Agent installed and started successfully!"); } function isRunningAsAdmin() { @@ -89,6 +94,22 @@ function runMsiInstaller(msiPath) { execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); } +function stopServiceIfRunning() { + try { + ui.writeInformation("Stopping existing SafeChain Agent service..."); + ui.writeVerbose('Running: net stop "SafeChainAgent"'); + execSync('net stop "SafeChainAgent"', { stdio: "inherit" }); + } catch { + // Service is not running or doesn't exist, which is fine + ui.writeVerbose("SafeChain Agent service not running or not installed."); + } +} + +function startService() { + ui.writeVerbose('Running: net start "SafeChainAgent"'); + execSync('net start "SafeChainAgent"', { stdio: "inherit" }); +} + /** * @param {string} msiPath */ From 6a3c7b938b9ac12d35924c30cf4c2159c901a7a8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 13:48:33 +0100 Subject: [PATCH 03/46] Overwrite the agent if it's already installed. --- packages/safe-chain/src/installation/installUltimate.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index a638407..5d38eb0 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -41,7 +41,7 @@ async function installOnWindows() { stopServiceIfRunning(); ui.writeInformation("Installing SafeChain Agent..."); - ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); + ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn REINSTALL=ALL REINSTALLMODE=vomus`); runMsiInstaller(msiPath); ui.writeInformation("Starting SafeChain Agent service..."); @@ -91,7 +91,10 @@ async function downloadFile(url, destPath) { * @param {string} msiPath */ function runMsiInstaller(msiPath) { - execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); + // Use /i for install/upgrade with REINSTALL=ALL REINSTALLMODE=vomus + // This forces a reinstall of all features if the product is already installed + // /qn = quiet mode (no UI) + execSync(`msiexec /i "${msiPath}" /qn REINSTALL=ALL REINSTALLMODE=vomus`, { stdio: "inherit" }); } function stopServiceIfRunning() { From 4851e582f69a89ddc6ae24420002105cd5b7f467 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 13:54:32 +0100 Subject: [PATCH 04/46] Improve updating existing agent install --- .../src/installation/installUltimate.js | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 5d38eb0..1860b80 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -24,7 +24,7 @@ async function installOnWindows() { if (!isRunningAsAdmin()) { ui.writeError("Administrator privileges required."); ui.writeInformation( - "Please run this command in an elevated terminal (Run as Administrator)." + "Please run this command in an elevated terminal (Run as Administrator).", ); return; } @@ -33,15 +33,20 @@ async function installOnWindows() { const downloadUrl = buildDownloadUrl(architecture); const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); - ui.writeInformation(`Downloading SafeChain Agent ${ULTIMATE_VERSION} for ${architecture}...`); + ui.writeInformation( + `Downloading SafeChain Agent ${ULTIMATE_VERSION} for ${architecture}...`, + ); ui.writeVerbose(`Download URL: ${downloadUrl}`); ui.writeVerbose(`Destination: ${msiPath}`); await downloadFile(downloadUrl, msiPath); stopServiceIfRunning(); + // Wait a moment for the service to fully stop before installing + await new Promise((resolve) => setTimeout(resolve, 10000)); + ui.writeInformation("Installing SafeChain Agent..."); - ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn REINSTALL=ALL REINSTALLMODE=vomus`); + ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn /norestart`); runMsiInstaller(msiPath); ui.writeInformation("Starting SafeChain Agent service..."); @@ -91,10 +96,23 @@ async function downloadFile(url, destPath) { * @param {string} msiPath */ function runMsiInstaller(msiPath) { - // Use /i for install/upgrade with REINSTALL=ALL REINSTALLMODE=vomus - // This forces a reinstall of all features if the product is already installed + // Try to install/upgrade + // /i = install (will upgrade if product code matches) // /qn = quiet mode (no UI) - execSync(`msiexec /i "${msiPath}" /qn REINSTALL=ALL REINSTALLMODE=vomus`, { stdio: "inherit" }); + // /norestart = suppress restarts + try { + execSync(`msiexec /i "${msiPath}" /qn /norestart`, { stdio: "inherit" }); + } catch (error) { + // If installation fails, it might be because it's already installed + // Try to force a reinstall + ui.writeVerbose( + "Initial installation failed, attempting to force reinstall...", + ); + execSync( + `msiexec /i "${msiPath}" /qn /norestart REINSTALL=ALL REINSTALLMODE=vomus`, + { stdio: "inherit" }, + ); + } } function stopServiceIfRunning() { From c4941e25ed5ca304e684b921a8e20590c81f8486 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 13:55:41 +0100 Subject: [PATCH 05/46] Fix linting --- packages/safe-chain/src/installation/installUltimate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 1860b80..278aab4 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -102,7 +102,7 @@ function runMsiInstaller(msiPath) { // /norestart = suppress restarts try { execSync(`msiexec /i "${msiPath}" /qn /norestart`, { stdio: "inherit" }); - } catch (error) { + } catch { // If installation fails, it might be because it's already installed // Try to force a reinstall ui.writeVerbose( From 673783ceabffac02ec684e3f661b1e90672669f6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 14:00:09 +0100 Subject: [PATCH 06/46] Uninstall safe-chain agent if it's there, before re-installing --- .../src/installation/installUltimate.js | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 278aab4..6cb3f46 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -41,9 +41,10 @@ async function installOnWindows() { await downloadFile(downloadUrl, msiPath); stopServiceIfRunning(); + uninstallIfInstalled(); - // Wait a moment for the service to fully stop before installing - await new Promise((resolve) => setTimeout(resolve, 10000)); + // Wait a moment for uninstall to complete + await new Promise((resolve) => setTimeout(resolve, 2000)); ui.writeInformation("Installing SafeChain Agent..."); ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn /norestart`); @@ -92,27 +93,25 @@ async function downloadFile(url, destPath) { await pipeline(response.body, createWriteStream(destPath)); } +function uninstallIfInstalled() { + try { + ui.writeInformation("Uninstalling existing SafeChain Agent..."); + ui.writeVerbose('Running: wmic product where "name=\'SafeChain Agent\'" call uninstall /nointeractive'); + execSync('wmic product where "name=\'SafeChain Agent\'" call uninstall /nointeractive', { stdio: "inherit" }); + } catch { + // Not installed or uninstall failed, which is fine for a fresh install + ui.writeVerbose("No existing SafeChain Agent installation found."); + } +} + /** * @param {string} msiPath */ function runMsiInstaller(msiPath) { - // Try to install/upgrade - // /i = install (will upgrade if product code matches) + // /i = install // /qn = quiet mode (no UI) // /norestart = suppress restarts - try { - execSync(`msiexec /i "${msiPath}" /qn /norestart`, { stdio: "inherit" }); - } catch { - // If installation fails, it might be because it's already installed - // Try to force a reinstall - ui.writeVerbose( - "Initial installation failed, attempting to force reinstall...", - ); - execSync( - `msiexec /i "${msiPath}" /qn /norestart REINSTALL=ALL REINSTALLMODE=vomus`, - { stdio: "inherit" }, - ); - } + execSync(`msiexec /i "${msiPath}" /qn /norestart`, { stdio: "inherit" }); } function stopServiceIfRunning() { From 3958fcfcefeaca7d9e4a215b54b433da02d4178d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 14:06:43 +0100 Subject: [PATCH 07/46] Parse cli args in ultimate installation --- .../src/installation/installUltimate.js | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 6cb3f46..fc2b93f 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -5,10 +5,13 @@ import { execSync } from "child_process"; import { pipeline } from "stream/promises"; import fetch from "make-fetch-happen"; import { ui } from "../environment/userInteraction.js"; +import { initializeCliArguments } from "../config/cliArguments.js"; const ULTIMATE_VERSION = "v0.2.0"; export function installUltimate() { + initializeCliArguments(process.argv); + const operatingSystem = platform(); if (operatingSystem === "win32") { @@ -96,8 +99,25 @@ async function downloadFile(url, destPath) { function uninstallIfInstalled() { try { ui.writeInformation("Uninstalling existing SafeChain Agent..."); - ui.writeVerbose('Running: wmic product where "name=\'SafeChain Agent\'" call uninstall /nointeractive'); - execSync('wmic product where "name=\'SafeChain Agent\'" call uninstall /nointeractive', { stdio: "inherit" }); + + // Use PowerShell to find the product code, then use msiexec to uninstall + // This is the modern alternative to wmic which is deprecated + const findProductCodeCmd = `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`; + ui.writeVerbose(`Finding product code: ${findProductCodeCmd}`); + + const productCode = execSync(findProductCodeCmd, { + encoding: "utf8", + }).trim(); + + if (productCode) { + ui.writeVerbose(`Found product code: ${productCode}`); + ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); + execSync(`msiexec /x ${productCode} /qn /norestart`, { + stdio: "inherit", + }); + } else { + ui.writeVerbose("No existing SafeChain Agent installation found."); + } } catch { // Not installed or uninstall failed, which is fine for a fresh install ui.writeVerbose("No existing SafeChain Agent installation found."); From 2784dfd34e89b87f92eb3ed683a821c299adbfb0 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 14:23:15 +0100 Subject: [PATCH 08/46] Check if the agents service is running before starting it --- .../safe-chain/src/installation/installUltimate.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index fc2b93f..dd8de84 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -146,6 +146,19 @@ function stopServiceIfRunning() { } function startService() { + try { + // Check if service is already running + ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); + const status = execSync('sc query "SafeChainAgent"', { encoding: "utf8" }); + + if (status.includes("RUNNING")) { + ui.writeVerbose("SafeChain Agent service is already running."); + return; + } + } catch { + // Service might not exist yet or query failed, proceed with start + } + ui.writeVerbose('Running: net start "SafeChainAgent"'); execSync('net start "SafeChainAgent"', { stdio: "inherit" }); } From 0e7cce750d7ddfaaef20f00ea3bbc595573163d3 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 14:30:09 +0100 Subject: [PATCH 09/46] Improve output --- .../src/installation/installUltimate.js | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index dd8de84..d1ccf28 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -36,29 +36,35 @@ async function installOnWindows() { const downloadUrl = buildDownloadUrl(architecture); const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); + ui.emptyLine(); ui.writeInformation( - `Downloading SafeChain Agent ${ULTIMATE_VERSION} for ${architecture}...`, + `๐Ÿ“ฅ Downloading SafeChain Agent ${ULTIMATE_VERSION} (${architecture})...`, ); ui.writeVerbose(`Download URL: ${downloadUrl}`); ui.writeVerbose(`Destination: ${msiPath}`); await downloadFile(downloadUrl, msiPath); + ui.emptyLine(); stopServiceIfRunning(); uninstallIfInstalled(); // Wait a moment for uninstall to complete await new Promise((resolve) => setTimeout(resolve, 2000)); - ui.writeInformation("Installing SafeChain Agent..."); + ui.writeInformation("โš™๏ธ Installing SafeChain Agent..."); ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn /norestart`); runMsiInstaller(msiPath); - ui.writeInformation("Starting SafeChain Agent service..."); + ui.emptyLine(); + ui.writeInformation("๐Ÿš€ Starting SafeChain Agent service..."); startService(); ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); cleanup(msiPath); - ui.writeInformation("SafeChain Agent installed and started successfully!"); + + ui.emptyLine(); + ui.writeInformation("โœ… SafeChain Agent installed and started successfully!"); + ui.emptyLine(); } function isRunningAsAdmin() { @@ -98,8 +104,6 @@ async function downloadFile(url, destPath) { function uninstallIfInstalled() { try { - ui.writeInformation("Uninstalling existing SafeChain Agent..."); - // Use PowerShell to find the product code, then use msiexec to uninstall // This is the modern alternative to wmic which is deprecated const findProductCodeCmd = `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`; @@ -110,17 +114,18 @@ function uninstallIfInstalled() { }).trim(); if (productCode) { + ui.writeInformation("๐Ÿ—‘๏ธ Removing previous installation..."); ui.writeVerbose(`Found product code: ${productCode}`); ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); execSync(`msiexec /x ${productCode} /qn /norestart`, { stdio: "inherit", }); } else { - ui.writeVerbose("No existing SafeChain Agent installation found."); + ui.writeVerbose("No existing installation found (fresh install)."); } } catch { // Not installed or uninstall failed, which is fine for a fresh install - ui.writeVerbose("No existing SafeChain Agent installation found."); + ui.writeVerbose("No existing installation found (fresh install)."); } } @@ -136,12 +141,12 @@ function runMsiInstaller(msiPath) { function stopServiceIfRunning() { try { - ui.writeInformation("Stopping existing SafeChain Agent service..."); + ui.writeInformation("โน๏ธ Stopping running service..."); ui.writeVerbose('Running: net stop "SafeChainAgent"'); execSync('net stop "SafeChainAgent"', { stdio: "inherit" }); } catch { // Service is not running or doesn't exist, which is fine - ui.writeVerbose("SafeChain Agent service not running or not installed."); + ui.writeVerbose("Service not running (will start after installation)."); } } From fd559cfc63779037152dc892fdcb34d70d42f4a3 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 14:46:04 +0100 Subject: [PATCH 10/46] Restructure code into separate files --- .../src/installation/downloadAgent.js | 40 +++++ .../src/installation/installOnWindows.js | 146 +++++++++++++++ .../src/installation/installUltimate.js | 166 +----------------- packages/safe-chain/src/main.js | 10 +- 4 files changed, 193 insertions(+), 169 deletions(-) create mode 100644 packages/safe-chain/src/installation/downloadAgent.js create mode 100644 packages/safe-chain/src/installation/installOnWindows.js diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js new file mode 100644 index 0000000..2e45b79 --- /dev/null +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -0,0 +1,40 @@ +import { createWriteStream } from "fs"; +import { pipeline } from "stream/promises"; +import fetch from "make-fetch-happen"; + +const ULTIMATE_VERSION = "v0.2.0"; + +/** + * @typedef {"windows"} Platform + * @typedef {"amd64" | "arm64"} Architecture + */ + +/** + * Builds the download URL for the SafeChain Agent installer. + * @param {Platform} platform + * @param {Architecture} architecture + */ +export function getAgentDownloadUrl(platform, architecture) { + const extension = platform === "windows" ? "msi" : "pkg"; + return `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-${platform}-${architecture}.${extension}`; +} + +/** + * Downloads a file from a URL to a local path. + * @param {string} url + * @param {string} destPath + */ +export async function downloadFile(url, destPath) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Download failed: ${response.statusText}`); + } + await pipeline(response.body, createWriteStream(destPath)); +} + +/** + * Returns the current agent version. + */ +export function getAgentVersion() { + return ULTIMATE_VERSION; +} diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js new file mode 100644 index 0000000..27db104 --- /dev/null +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -0,0 +1,146 @@ +import { arch, tmpdir } from "os"; +import { unlinkSync } from "fs"; +import { join } from "path"; +import { execSync } from "child_process"; +import { ui } from "../environment/userInteraction.js"; +import { + getAgentDownloadUrl, + getAgentVersion, + downloadFile, +} from "./downloadAgent.js"; + +export async function installOnWindows() { + if (!isRunningAsAdmin()) { + ui.writeError("Administrator privileges required."); + ui.writeInformation( + "Please run this command in an elevated terminal (Run as Administrator).", + ); + return; + } + + const architecture = getWindowsArchitecture(); + const downloadUrl = getAgentDownloadUrl("windows", architecture); + const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); + + ui.emptyLine(); + ui.writeInformation( + `๐Ÿ“ฅ Downloading SafeChain Agent ${getAgentVersion()} (${architecture})...`, + ); + ui.writeVerbose(`Download URL: ${downloadUrl}`); + ui.writeVerbose(`Destination: ${msiPath}`); + await downloadFile(downloadUrl, msiPath); + + ui.emptyLine(); + stopServiceIfRunning(); + uninstallIfInstalled(); + + // Wait a moment for uninstall to complete + await new Promise((resolve) => setTimeout(resolve, 2000)); + + ui.writeInformation("โš™๏ธ Installing SafeChain Agent..."); + runMsiInstaller(msiPath); + + ui.emptyLine(); + ui.writeInformation("๐Ÿš€ Starting SafeChain Agent service..."); + startService(); + + ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); + cleanup(msiPath); + + ui.emptyLine(); + ui.writeInformation("โœ… SafeChain Agent installed and started successfully!"); + ui.emptyLine(); +} + +function isRunningAsAdmin() { + try { + execSync("net session", { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +function getWindowsArchitecture() { + const nodeArch = arch(); + if (nodeArch === "x64") return "amd64"; + if (nodeArch === "arm64") return "arm64"; + throw new Error(`Unsupported architecture: ${nodeArch}`); +} + +function uninstallIfInstalled() { + try { + // Use PowerShell to find the product code, then use msiexec to uninstall + // This is the modern alternative to wmic which is deprecated + const findProductCodeCmd = `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`; + ui.writeVerbose(`Finding product code: ${findProductCodeCmd}`); + + const productCode = execSync(findProductCodeCmd, { + encoding: "utf8", + }).trim(); + + if (productCode) { + ui.writeInformation("๐Ÿ—‘๏ธ Removing previous installation..."); + ui.writeVerbose(`Found product code: ${productCode}`); + ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); + execSync(`msiexec /x ${productCode} /qn /norestart`, { + stdio: "inherit", + }); + } else { + ui.writeVerbose("No existing installation found (fresh install)."); + } + } catch { + // Not installed or uninstall failed, which is fine for a fresh install + ui.writeVerbose("No existing installation found (fresh install)."); + } +} + +/** + * @param {string} msiPath + */ +function runMsiInstaller(msiPath) { + // /i = install + // /qn = quiet mode (no UI) + ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); + execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); +} + +function stopServiceIfRunning() { + try { + ui.writeInformation("โน๏ธ Stopping running service..."); + ui.writeVerbose('Running: net stop "SafeChainAgent"'); + execSync('net stop "SafeChainAgent"', { stdio: "inherit" }); + } catch { + // Service is not running or doesn't exist, which is fine + ui.writeVerbose("Service not running (will start after installation)."); + } +} + +function startService() { + try { + // Check if service is already running + ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); + const status = execSync('sc query "SafeChainAgent"', { encoding: "utf8" }); + + if (status.includes("RUNNING")) { + ui.writeVerbose("SafeChain Agent service is already running."); + return; + } + } catch { + // Service might not exist yet or query failed, proceed with start + } + + ui.writeVerbose('Running: net start "SafeChainAgent"'); + execSync('net start "SafeChainAgent"', { stdio: "inherit" }); +} + +/** + * @param {string} msiPath + */ +function cleanup(msiPath) { + try { + unlinkSync(msiPath); + } catch { + // Ignore cleanup errors + } +} diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index d1ccf28..7383d2c 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -1,13 +1,7 @@ -import { platform, arch, tmpdir } from "os"; -import { createWriteStream, unlinkSync } from "fs"; -import { join } from "path"; -import { execSync } from "child_process"; -import { pipeline } from "stream/promises"; -import fetch from "make-fetch-happen"; +import { platform } from "os"; import { ui } from "../environment/userInteraction.js"; import { initializeCliArguments } from "../config/cliArguments.js"; - -const ULTIMATE_VERSION = "v0.2.0"; +import { installOnWindows } from "./installOnWindows.js"; export function installUltimate() { initializeCliArguments(process.argv); @@ -22,159 +16,3 @@ export function installUltimate() { ); } } - -async function installOnWindows() { - if (!isRunningAsAdmin()) { - ui.writeError("Administrator privileges required."); - ui.writeInformation( - "Please run this command in an elevated terminal (Run as Administrator).", - ); - return; - } - - const architecture = getWindowsArchitecture(); - const downloadUrl = buildDownloadUrl(architecture); - const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); - - ui.emptyLine(); - ui.writeInformation( - `๐Ÿ“ฅ Downloading SafeChain Agent ${ULTIMATE_VERSION} (${architecture})...`, - ); - ui.writeVerbose(`Download URL: ${downloadUrl}`); - ui.writeVerbose(`Destination: ${msiPath}`); - await downloadFile(downloadUrl, msiPath); - - ui.emptyLine(); - stopServiceIfRunning(); - uninstallIfInstalled(); - - // Wait a moment for uninstall to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); - - ui.writeInformation("โš™๏ธ Installing SafeChain Agent..."); - ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn /norestart`); - runMsiInstaller(msiPath); - - ui.emptyLine(); - ui.writeInformation("๐Ÿš€ Starting SafeChain Agent service..."); - startService(); - - ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); - cleanup(msiPath); - - ui.emptyLine(); - ui.writeInformation("โœ… SafeChain Agent installed and started successfully!"); - ui.emptyLine(); -} - -function isRunningAsAdmin() { - try { - execSync("net session", { stdio: "ignore" }); - return true; - } catch { - return false; - } -} - -function getWindowsArchitecture() { - const nodeArch = arch(); - if (nodeArch === "x64") return "amd64"; - if (nodeArch === "arm64") return "arm64"; - throw new Error(`Unsupported architecture: ${nodeArch}`); -} - -/** - * @param {string} architecture - */ -function buildDownloadUrl(architecture) { - return `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-windows-${architecture}.msi`; -} - -/** - * @param {string} url - * @param {string} destPath - */ -async function downloadFile(url, destPath) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Download failed: ${response.statusText}`); - } - await pipeline(response.body, createWriteStream(destPath)); -} - -function uninstallIfInstalled() { - try { - // Use PowerShell to find the product code, then use msiexec to uninstall - // This is the modern alternative to wmic which is deprecated - const findProductCodeCmd = `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`; - ui.writeVerbose(`Finding product code: ${findProductCodeCmd}`); - - const productCode = execSync(findProductCodeCmd, { - encoding: "utf8", - }).trim(); - - if (productCode) { - ui.writeInformation("๐Ÿ—‘๏ธ Removing previous installation..."); - ui.writeVerbose(`Found product code: ${productCode}`); - ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); - execSync(`msiexec /x ${productCode} /qn /norestart`, { - stdio: "inherit", - }); - } else { - ui.writeVerbose("No existing installation found (fresh install)."); - } - } catch { - // Not installed or uninstall failed, which is fine for a fresh install - ui.writeVerbose("No existing installation found (fresh install)."); - } -} - -/** - * @param {string} msiPath - */ -function runMsiInstaller(msiPath) { - // /i = install - // /qn = quiet mode (no UI) - // /norestart = suppress restarts - execSync(`msiexec /i "${msiPath}" /qn /norestart`, { stdio: "inherit" }); -} - -function stopServiceIfRunning() { - try { - ui.writeInformation("โน๏ธ Stopping running service..."); - ui.writeVerbose('Running: net stop "SafeChainAgent"'); - execSync('net stop "SafeChainAgent"', { stdio: "inherit" }); - } catch { - // Service is not running or doesn't exist, which is fine - ui.writeVerbose("Service not running (will start after installation)."); - } -} - -function startService() { - try { - // Check if service is already running - ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); - const status = execSync('sc query "SafeChainAgent"', { encoding: "utf8" }); - - if (status.includes("RUNNING")) { - ui.writeVerbose("SafeChain Agent service is already running."); - return; - } - } catch { - // Service might not exist yet or query failed, proceed with start - } - - ui.writeVerbose('Running: net start "SafeChainAgent"'); - execSync('net start "SafeChainAgent"', { stdio: "inherit" }); -} - -/** - * @param {string} msiPath - */ -function cleanup(msiPath) { - try { - unlinkSync(msiPath); - } catch { - // Ignore cleanup errors - } -} diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 9b7ba53..0b37eba 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -73,20 +73,20 @@ export async function main(args) { ui.writeVerbose( `${chalk.green("โœ”")} Safe-chain: Scanned ${ auditStats.totalPackages - } packages, no malware found.` + } packages, no malware found.`, ); } if (proxy.hasSuppressedVersions()) { ui.writeInformation( `${chalk.yellow( - "โ„น" - )} Safe-chain: Some package versions were suppressed due to minimum age requirement.` + "โ„น", + )} Safe-chain: Some package versions were suppressed due to minimum age requirement.`, ); ui.writeInformation( ` To disable this check, use: ${chalk.cyan( - "--safe-chain-skip-minimum-package-age" - )}` + "--safe-chain-skip-minimum-package-age", + )}`, ); } From 079e4893b1e55d43f8c772d9246588bb5184f7a2 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 14:53:33 +0100 Subject: [PATCH 11/46] Move download name construction to os installer function --- .../safe-chain/src/installation/downloadAgent.js | 13 +++---------- .../safe-chain/src/installation/installOnWindows.js | 3 ++- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index 2e45b79..d74cbf7 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -4,19 +4,12 @@ import fetch from "make-fetch-happen"; const ULTIMATE_VERSION = "v0.2.0"; -/** - * @typedef {"windows"} Platform - * @typedef {"amd64" | "arm64"} Architecture - */ - /** * Builds the download URL for the SafeChain Agent installer. - * @param {Platform} platform - * @param {Architecture} architecture + * @param {string} fileName */ -export function getAgentDownloadUrl(platform, architecture) { - const extension = platform === "windows" ? "msi" : "pkg"; - return `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-${platform}-${architecture}.${extension}`; +export function getAgentDownloadUrl(fileName) { + return `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/${fileName}`; } /** diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 27db104..9f92893 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -19,7 +19,8 @@ export async function installOnWindows() { } const architecture = getWindowsArchitecture(); - const downloadUrl = getAgentDownloadUrl("windows", architecture); + const fileName = `SafeChainAgent-windows-${architecture}.msi`; + const downloadUrl = getAgentDownloadUrl(fileName); const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); ui.emptyLine(); From 471ef2821015309ecdd82c8400f27a33a7d52793 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:22:24 +0100 Subject: [PATCH 12/46] Handle code quality comments --- packages/safe-chain/bin/safe-chain.js | 4 +++- packages/safe-chain/src/installation/installOnWindows.js | 4 ++-- packages/safe-chain/src/installation/installUltimate.js | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index e33ad9f..d048ce1 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -64,7 +64,9 @@ if (tool) { } else if (command === "setup") { setup(); } else if (command === "--ultimate") { - installUltimate(); + (async () => { + await installUltimate(); + })(); } else if (command === "teardown") { teardownDirectories(); teardown(); diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 9f92893..094ec58 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -103,7 +103,7 @@ function runMsiInstaller(msiPath) { // /i = install // /qn = quiet mode (no UI) ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); - execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); + execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); // noopengrep this is ok, we control the msiPath } function stopServiceIfRunning() { @@ -128,7 +128,7 @@ function startService() { return; } } catch { - // Service might not exist yet or query failed, proceed with start + ui.writeVerbose("Service not found or query failed, attempting to start."); } ui.writeVerbose('Running: net start "SafeChainAgent"'); diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 7383d2c..3b6846a 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -3,13 +3,13 @@ import { ui } from "../environment/userInteraction.js"; import { initializeCliArguments } from "../config/cliArguments.js"; import { installOnWindows } from "./installOnWindows.js"; -export function installUltimate() { +export async function installUltimate() { initializeCliArguments(process.argv); const operatingSystem = platform(); if (operatingSystem === "win32") { - installOnWindows(); + await installOnWindows(); } else { ui.writeInformation( `${operatingSystem} is not supported yet by safe-chain's ultimate version.`, From 9b61a325fa1eccb98becd11088ba9763da4c2d1c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:24:49 +0100 Subject: [PATCH 13/46] Log when installer file cleanup failed --- packages/safe-chain/src/installation/installOnWindows.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 094ec58..f3c9ee8 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -142,6 +142,6 @@ function cleanup(msiPath) { try { unlinkSync(msiPath); } catch { - // Ignore cleanup errors + ui.writeVerbose("Failed to clean up temporary installer file."); } } From 8b189443b7e293ad7f4bc88944da577ce2ace311 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:31:41 +0100 Subject: [PATCH 14/46] Use safeSpawn instead of execSync --- .../src/installation/installOnWindows.js | 109 +++++++++--------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index f3c9ee8..54893bf 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -1,8 +1,8 @@ import { arch, tmpdir } from "os"; import { unlinkSync } from "fs"; import { join } from "path"; -import { execSync } from "child_process"; import { ui } from "../environment/userInteraction.js"; +import { safeSpawn } from "../utils/safeSpawn.js"; import { getAgentDownloadUrl, getAgentVersion, @@ -10,7 +10,7 @@ import { } from "./downloadAgent.js"; export async function installOnWindows() { - if (!isRunningAsAdmin()) { + if (!(await isRunningAsAdmin())) { ui.writeError("Administrator privileges required."); ui.writeInformation( "Please run this command in an elevated terminal (Run as Administrator).", @@ -32,18 +32,18 @@ export async function installOnWindows() { await downloadFile(downloadUrl, msiPath); ui.emptyLine(); - stopServiceIfRunning(); - uninstallIfInstalled(); + await stopServiceIfRunning(); + await uninstallIfInstalled(); // Wait a moment for uninstall to complete await new Promise((resolve) => setTimeout(resolve, 2000)); ui.writeInformation("โš™๏ธ Installing SafeChain Agent..."); - runMsiInstaller(msiPath); + await runMsiInstaller(msiPath); ui.emptyLine(); ui.writeInformation("๐Ÿš€ Starting SafeChain Agent service..."); - startService(); + await startService(); ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); cleanup(msiPath); @@ -53,13 +53,9 @@ export async function installOnWindows() { ui.emptyLine(); } -function isRunningAsAdmin() { - try { - execSync("net session", { stdio: "ignore" }); - return true; - } catch { - return false; - } +async function isRunningAsAdmin() { + const result = await safeSpawn("net", ["session"], { stdio: "ignore" }); + return result.status === 0; } function getWindowsArchitecture() { @@ -69,29 +65,31 @@ function getWindowsArchitecture() { throw new Error(`Unsupported architecture: ${nodeArch}`); } -function uninstallIfInstalled() { - try { - // Use PowerShell to find the product code, then use msiexec to uninstall - // This is the modern alternative to wmic which is deprecated - const findProductCodeCmd = `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`; - ui.writeVerbose(`Finding product code: ${findProductCodeCmd}`); +async function uninstallIfInstalled() { + // Use PowerShell to find the product code, then use msiexec to uninstall + // This is the modern alternative to wmic which is deprecated + const powershellScript = `$app = Get-WmiObject -Class Win32_Product -Filter "Name='SafeChain Agent'"; if ($app) { Write-Output $app.IdentifyingNumber }`; + ui.writeVerbose(`Finding product code with PowerShell`); - const productCode = execSync(findProductCodeCmd, { - encoding: "utf8", - }).trim(); + const result = await safeSpawn("powershell", ["-Command", powershellScript], { + stdio: "pipe", + }); - if (productCode) { - ui.writeInformation("๐Ÿ—‘๏ธ Removing previous installation..."); - ui.writeVerbose(`Found product code: ${productCode}`); - ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); - execSync(`msiexec /x ${productCode} /qn /norestart`, { - stdio: "inherit", - }); - } else { - ui.writeVerbose("No existing installation found (fresh install)."); - } - } catch { - // Not installed or uninstall failed, which is fine for a fresh install + if (result.status !== 0) { + ui.writeVerbose("No existing installation found (fresh install)."); + return; + } + + const productCode = result.stdout.trim(); + + if (productCode) { + ui.writeInformation("๐Ÿ—‘๏ธ Removing previous installation..."); + ui.writeVerbose(`Found product code: ${productCode}`); + ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); + await safeSpawn("msiexec", ["/x", productCode, "/qn", "/norestart"], { + stdio: "inherit", + }); + } else { ui.writeVerbose("No existing installation found (fresh install)."); } } @@ -99,40 +97,43 @@ function uninstallIfInstalled() { /** * @param {string} msiPath */ -function runMsiInstaller(msiPath) { +async function runMsiInstaller(msiPath) { // /i = install // /qn = quiet mode (no UI) ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); - execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); // noopengrep this is ok, we control the msiPath + await safeSpawn("msiexec", ["/i", msiPath, "/qn"], { stdio: "inherit" }); } -function stopServiceIfRunning() { - try { - ui.writeInformation("โน๏ธ Stopping running service..."); - ui.writeVerbose('Running: net stop "SafeChainAgent"'); - execSync('net stop "SafeChainAgent"', { stdio: "inherit" }); - } catch { - // Service is not running or doesn't exist, which is fine +async function stopServiceIfRunning() { + ui.writeInformation("โน๏ธ Stopping running service..."); + ui.writeVerbose('Running: net stop "SafeChainAgent"'); + const result = await safeSpawn("net", ["stop", "SafeChainAgent"], { + stdio: "inherit", + }); + + if (result.status !== 0) { ui.writeVerbose("Service not running (will start after installation)."); } } -function startService() { - try { - // Check if service is already running - ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); - const status = execSync('sc query "SafeChainAgent"', { encoding: "utf8" }); +async function startService() { + // Check if service is already running + ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); + const queryResult = await safeSpawn("sc", ["query", "SafeChainAgent"], { + stdio: "pipe", + }); - if (status.includes("RUNNING")) { - ui.writeVerbose("SafeChain Agent service is already running."); - return; - } - } catch { + if (queryResult.status === 0 && queryResult.stdout.includes("RUNNING")) { + ui.writeVerbose("SafeChain Agent service is already running."); + return; + } + + if (queryResult.status !== 0) { ui.writeVerbose("Service not found or query failed, attempting to start."); } ui.writeVerbose('Running: net start "SafeChainAgent"'); - execSync('net start "SafeChainAgent"', { stdio: "inherit" }); + await safeSpawn("net", ["start", "SafeChainAgent"], { stdio: "inherit" }); } /** From 4a90bd262109d307e0767eed654258a3e90801bd Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:34:16 +0100 Subject: [PATCH 15/46] Code quality: use early return --- .../src/installation/installOnWindows.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 54893bf..a6e0938 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -82,16 +82,17 @@ async function uninstallIfInstalled() { const productCode = result.stdout.trim(); - if (productCode) { - ui.writeInformation("๐Ÿ—‘๏ธ Removing previous installation..."); - ui.writeVerbose(`Found product code: ${productCode}`); - ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); - await safeSpawn("msiexec", ["/x", productCode, "/qn", "/norestart"], { - stdio: "inherit", - }); - } else { + if (!productCode) { ui.writeVerbose("No existing installation found (fresh install)."); + return; } + + ui.writeInformation("๐Ÿ—‘๏ธ Removing previous installation..."); + ui.writeVerbose(`Found product code: ${productCode}`); + ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); + await safeSpawn("msiexec", ["/x", productCode, "/qn", "/norestart"], { + stdio: "inherit", + }); } /** From 86e600773370e0b5da5f743ba1840bd008899201 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:45:32 +0100 Subject: [PATCH 16/46] Improve error handling --- .../src/installation/installOnWindows.js | 93 ++++++++++++------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index a6e0938..6db1cb0 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -31,31 +31,43 @@ export async function installOnWindows() { ui.writeVerbose(`Destination: ${msiPath}`); await downloadFile(downloadUrl, msiPath); - ui.emptyLine(); - await stopServiceIfRunning(); - await uninstallIfInstalled(); + try { + ui.emptyLine(); + await stopServiceIfRunning(); + await uninstallIfInstalled(); - // Wait a moment for uninstall to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); + // Wait a moment for uninstall to complete + await new Promise((resolve) => setTimeout(resolve, 2000)); - ui.writeInformation("โš™๏ธ Installing SafeChain Agent..."); - await runMsiInstaller(msiPath); + ui.writeInformation("โš™๏ธ Installing SafeChain Agent..."); + await runMsiInstaller(msiPath); - ui.emptyLine(); - ui.writeInformation("๐Ÿš€ Starting SafeChain Agent service..."); - await startService(); + ui.emptyLine(); + ui.writeInformation("๐Ÿš€ Starting SafeChain Agent service..."); + await startService(); - ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); - cleanup(msiPath); - - ui.emptyLine(); - ui.writeInformation("โœ… SafeChain Agent installed and started successfully!"); - ui.emptyLine(); + ui.emptyLine(); + ui.writeInformation( + "โœ… SafeChain Agent installed and started successfully!", + ); + ui.emptyLine(); + } finally { + ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); + cleanup(msiPath); + } } async function isRunningAsAdmin() { - const result = await safeSpawn("net", ["session"], { stdio: "ignore" }); - return result.status === 0; + const result = await safeSpawn( + "powershell", + [ + "-Command", + "([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)", + ], + { stdio: "pipe" }, + ); + + return result.status === 0 && result.stdout.trim() === "True"; } function getWindowsArchitecture() { @@ -66,8 +78,6 @@ function getWindowsArchitecture() { } async function uninstallIfInstalled() { - // Use PowerShell to find the product code, then use msiexec to uninstall - // This is the modern alternative to wmic which is deprecated const powershellScript = `$app = Get-WmiObject -Class Win32_Product -Filter "Name='SafeChain Agent'"; if ($app) { Write-Output $app.IdentifyingNumber }`; ui.writeVerbose(`Finding product code with PowerShell`); @@ -81,7 +91,6 @@ async function uninstallIfInstalled() { } const productCode = result.stdout.trim(); - if (!productCode) { ui.writeVerbose("No existing installation found (fresh install)."); return; @@ -89,27 +98,38 @@ async function uninstallIfInstalled() { ui.writeInformation("๐Ÿ—‘๏ธ Removing previous installation..."); ui.writeVerbose(`Found product code: ${productCode}`); - ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); - await safeSpawn("msiexec", ["/x", productCode, "/qn", "/norestart"], { - stdio: "inherit", - }); + + const uninstallResult = await safeSpawn( + "msiexec", + ["/x", productCode, "/qn", "/norestart"], + { stdio: "inherit" }, + ); + + if (uninstallResult.status !== 0) { + throw new Error(`Uninstall failed (exit code: ${uninstallResult.status})`); + } } /** * @param {string} msiPath */ async function runMsiInstaller(msiPath) { - // /i = install - // /qn = quiet mode (no UI) ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); - await safeSpawn("msiexec", ["/i", msiPath, "/qn"], { stdio: "inherit" }); + + const result = await safeSpawn("msiexec", ["/i", msiPath, "/qn"], { + stdio: "inherit", + }); + + if (result.status !== 0) { + throw new Error(`MSI installer failed (exit code: ${result.status})`); + } } async function stopServiceIfRunning() { ui.writeInformation("โน๏ธ Stopping running service..."); - ui.writeVerbose('Running: net stop "SafeChainAgent"'); + const result = await safeSpawn("net", ["stop", "SafeChainAgent"], { - stdio: "inherit", + stdio: "pipe", }); if (result.status !== 0) { @@ -118,7 +138,6 @@ async function stopServiceIfRunning() { } async function startService() { - // Check if service is already running ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); const queryResult = await safeSpawn("sc", ["query", "SafeChainAgent"], { stdio: "pipe", @@ -129,12 +148,14 @@ async function startService() { return; } - if (queryResult.status !== 0) { - ui.writeVerbose("Service not found or query failed, attempting to start."); - } - ui.writeVerbose('Running: net start "SafeChainAgent"'); - await safeSpawn("net", ["start", "SafeChainAgent"], { stdio: "inherit" }); + const startResult = await safeSpawn("net", ["start", "SafeChainAgent"], { + stdio: "pipe", + }); + + if (startResult.status !== 0) { + throw new Error(`Failed to start service (exit code: ${startResult.status})`); + } } /** From eb00fe6f3d8a17ec9e8ecf522db15f05072adc06 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:54:02 +0100 Subject: [PATCH 17/46] Write error output --- packages/safe-chain/src/installation/installOnWindows.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 6db1cb0..8610394 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -106,6 +106,8 @@ async function uninstallIfInstalled() { ); if (uninstallResult.status !== 0) { + ui.writeInformation(uninstallResult.stdout); + ui.writeInformation(uninstallResult.stderr); throw new Error(`Uninstall failed (exit code: ${uninstallResult.status})`); } } @@ -154,7 +156,9 @@ async function startService() { }); if (startResult.status !== 0) { - throw new Error(`Failed to start service (exit code: ${startResult.status})`); + throw new Error( + `Failed to start service (exit code: ${startResult.status})`, + ); } } From 4ebbbca4326295f1d144e0fd22c08cc03690ffbb Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:58:11 +0100 Subject: [PATCH 18/46] Temporarily disable cleanup --- .../src/installation/installOnWindows.js | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 8610394..0257fd8 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -31,30 +31,29 @@ export async function installOnWindows() { ui.writeVerbose(`Destination: ${msiPath}`); await downloadFile(downloadUrl, msiPath); - try { - ui.emptyLine(); - await stopServiceIfRunning(); - await uninstallIfInstalled(); + // try { + ui.emptyLine(); + await stopServiceIfRunning(); + await uninstallIfInstalled(); - // Wait a moment for uninstall to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); + // Wait a moment for uninstall to complete + await new Promise((resolve) => setTimeout(resolve, 2000)); - ui.writeInformation("โš™๏ธ Installing SafeChain Agent..."); - await runMsiInstaller(msiPath); + ui.writeInformation("โš™๏ธ Installing SafeChain Agent..."); + await runMsiInstaller(msiPath); - ui.emptyLine(); - ui.writeInformation("๐Ÿš€ Starting SafeChain Agent service..."); - await startService(); + ui.emptyLine(); + ui.writeInformation("๐Ÿš€ Starting SafeChain Agent service..."); + await startService(); - ui.emptyLine(); - ui.writeInformation( - "โœ… SafeChain Agent installed and started successfully!", - ); - ui.emptyLine(); - } finally { - ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); - cleanup(msiPath); - } + ui.emptyLine(); + ui.writeInformation("โœ… SafeChain Agent installed and started successfully!"); + ui.emptyLine(); + // } + // finally { + // ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); + // cleanup(msiPath); + // } } async function isRunningAsAdmin() { From 211f877384950e40a4c0e814c291b49517e7a066 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 16:03:51 +0100 Subject: [PATCH 19/46] Write stdout stderr --- .../src/installation/installOnWindows.js | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 0257fd8..7511eef 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -31,29 +31,30 @@ export async function installOnWindows() { ui.writeVerbose(`Destination: ${msiPath}`); await downloadFile(downloadUrl, msiPath); - // try { - ui.emptyLine(); - await stopServiceIfRunning(); - await uninstallIfInstalled(); + try { + ui.emptyLine(); + await stopServiceIfRunning(); + await uninstallIfInstalled(); - // Wait a moment for uninstall to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); + // Wait a moment for uninstall to complete + await new Promise((resolve) => setTimeout(resolve, 2000)); - ui.writeInformation("โš™๏ธ Installing SafeChain Agent..."); - await runMsiInstaller(msiPath); + ui.writeInformation("โš™๏ธ Installing SafeChain Agent..."); + await runMsiInstaller(msiPath); - ui.emptyLine(); - ui.writeInformation("๐Ÿš€ Starting SafeChain Agent service..."); - await startService(); + ui.emptyLine(); + ui.writeInformation("๐Ÿš€ Starting SafeChain Agent service..."); + await startService(); - ui.emptyLine(); - ui.writeInformation("โœ… SafeChain Agent installed and started successfully!"); - ui.emptyLine(); - // } - // finally { - // ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); - // cleanup(msiPath); - // } + ui.emptyLine(); + ui.writeInformation( + "โœ… SafeChain Agent installed and started successfully!", + ); + ui.emptyLine(); + } finally { + ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); + cleanup(msiPath); + } } async function isRunningAsAdmin() { @@ -85,7 +86,9 @@ async function uninstallIfInstalled() { }); if (result.status !== 0) { - ui.writeVerbose("No existing installation found (fresh install)."); + ui.writeVerbose( + `No existing installation found (fresh install). Output: ${result.stdout} ${result.stderr}`, + ); return; } From 4a7629a17487217500f2b8054e970eb7ca83e186 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 16:11:51 +0100 Subject: [PATCH 20/46] Use execSync to execute powershell command --- .../src/installation/installOnWindows.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 7511eef..84dcd9c 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -1,6 +1,7 @@ import { arch, tmpdir } from "os"; import { unlinkSync } from "fs"; import { join } from "path"; +import { execSync } from "child_process"; import { ui } from "../environment/userInteraction.js"; import { safeSpawn } from "../utils/safeSpawn.js"; import { @@ -81,18 +82,15 @@ async function uninstallIfInstalled() { const powershellScript = `$app = Get-WmiObject -Class Win32_Product -Filter "Name='SafeChain Agent'"; if ($app) { Write-Output $app.IdentifyingNumber }`; ui.writeVerbose(`Finding product code with PowerShell`); - const result = await safeSpawn("powershell", ["-Command", powershellScript], { - stdio: "pipe", - }); - - if (result.status !== 0) { - ui.writeVerbose( - `No existing installation found (fresh install). Output: ${result.stdout} ${result.stderr}`, - ); + let productCode; + try { + productCode = execSync(`powershell -Command "${powershellScript}"`, { + encoding: "utf8", + }).trim(); + } catch { + ui.writeVerbose("No existing installation found (fresh install)."); return; } - - const productCode = result.stdout.trim(); if (!productCode) { ui.writeVerbose("No existing installation found (fresh install)."); return; From 20fb949a2351d6dc1940b0294f74036b9cb10a4f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 16:17:34 +0100 Subject: [PATCH 21/46] Fix uninstall --- packages/safe-chain/src/installation/installOnWindows.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 84dcd9c..d6e7207 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -79,14 +79,14 @@ function getWindowsArchitecture() { } async function uninstallIfInstalled() { - const powershellScript = `$app = Get-WmiObject -Class Win32_Product -Filter "Name='SafeChain Agent'"; if ($app) { Write-Output $app.IdentifyingNumber }`; ui.writeVerbose(`Finding product code with PowerShell`); let productCode; try { - productCode = execSync(`powershell -Command "${powershellScript}"`, { - encoding: "utf8", - }).trim(); + productCode = execSync( + `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`, + { encoding: "utf8" }, + ).trim(); } catch { ui.writeVerbose("No existing installation found (fresh install)."); return; From c200ea56cf4aef90290654b2d1b40e1787515ac1 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 16:23:59 +0100 Subject: [PATCH 22/46] Cleanup debug logging --- packages/safe-chain/src/installation/installOnWindows.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index d6e7207..60a339b 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -106,8 +106,6 @@ async function uninstallIfInstalled() { ); if (uninstallResult.status !== 0) { - ui.writeInformation(uninstallResult.stdout); - ui.writeInformation(uninstallResult.stderr); throw new Error(`Uninstall failed (exit code: ${uninstallResult.status})`); } } From da6c022ef49c5fcb6faba43a1c6bb9d685a8cc37 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 16:25:50 +0100 Subject: [PATCH 23/46] Add explaining comments for powershell scritps --- packages/safe-chain/src/installation/installOnWindows.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 60a339b..41bb1ca 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -59,6 +59,8 @@ export async function installOnWindows() { } async function isRunningAsAdmin() { + // Uses Windows Security API to check if current process has admin privileges. + // Returns "True" or "False" as a string. const result = await safeSpawn( "powershell", [ @@ -79,6 +81,8 @@ function getWindowsArchitecture() { } async function uninstallIfInstalled() { + // Query Win32_Product via WMI to find the installed SafeChain Agent. + // If found, outputs the product GUID (e.g., "{12345678-1234-...}") needed for msiexec uninstall. ui.writeVerbose(`Finding product code with PowerShell`); let productCode; From 9651e05f4b04773dcd9029a93ce7aa97b2c49859 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Mon, 19 Jan 2026 18:59:37 +0100 Subject: [PATCH 24/46] Fix naming of SafeChain Agent --- packages/safe-chain/src/installation/installOnWindows.js | 8 ++++---- packages/safe-chain/src/installation/installUltimate.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 41bb1ca..9837d18 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -40,16 +40,16 @@ export async function installOnWindows() { // Wait a moment for uninstall to complete await new Promise((resolve) => setTimeout(resolve, 2000)); - ui.writeInformation("โš™๏ธ Installing SafeChain Agent..."); + ui.writeInformation("โš™๏ธ Installing SafeChain Ultimate..."); await runMsiInstaller(msiPath); ui.emptyLine(); - ui.writeInformation("๐Ÿš€ Starting SafeChain Agent service..."); + ui.writeInformation("๐Ÿš€ Starting SafeChain Ultimate service..."); await startService(); ui.emptyLine(); ui.writeInformation( - "โœ… SafeChain Agent installed and started successfully!", + "โœ… SafeChain Ultimate installed and started successfully!", ); ui.emptyLine(); } finally { @@ -148,7 +148,7 @@ async function startService() { }); if (queryResult.status === 0 && queryResult.stdout.includes("RUNNING")) { - ui.writeVerbose("SafeChain Agent service is already running."); + ui.writeVerbose("SafeChain Ultimate service is already running."); return; } diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 3b6846a..086b6a4 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -12,7 +12,7 @@ export async function installUltimate() { await installOnWindows(); } else { ui.writeInformation( - `${operatingSystem} is not supported yet by safe-chain's ultimate version.`, + `${operatingSystem} is not supported yet by SafeChain's ultimate version.`, ); } } From 3dad1c2516bdc26141a57eeb0eae24eb37f42ecc Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Mon, 19 Jan 2026 19:01:28 +0100 Subject: [PATCH 25/46] Update packages/safe-chain/src/installation/installOnWindows.js --- packages/safe-chain/src/installation/installOnWindows.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 9837d18..3cd3428 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -26,7 +26,7 @@ export async function installOnWindows() { ui.emptyLine(); ui.writeInformation( - `๐Ÿ“ฅ Downloading SafeChain Agent ${getAgentVersion()} (${architecture})...`, + `๐Ÿ“ฅ Downloading SafeChain Ultimate ${getAgentVersion()} (${architecture})...`, ); ui.writeVerbose(`Download URL: ${downloadUrl}`); ui.writeVerbose(`Destination: ${msiPath}`); From 7d55c5453bc19ac8ab203d6f37922c369627659a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 20 Jan 2026 09:12:00 +0100 Subject: [PATCH 26/46] Move os and arch detection to downloader, add checksum verification. --- .../src/installation/downloadAgent.js | 82 ++++++++++++++++++- .../src/installation/installOnWindows.js | 49 +++++------ 2 files changed, 102 insertions(+), 29 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index d74cbf7..2441f7d 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -1,9 +1,25 @@ -import { createWriteStream } from "fs"; +import { createWriteStream, createReadStream } from "fs"; +import { createHash } from "crypto"; import { pipeline } from "stream/promises"; import fetch from "make-fetch-happen"; const ULTIMATE_VERSION = "v0.2.0"; +const DOWNLOAD_URLS = { + win32: { + x64: { + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/SafeChainAgent-windows-amd64.msi", + checksum: + "sha256:c699f74a3666d85b70b8ede076a2192a6a023f1b395e8e6c7556927ee698a020", + }, + arm64: { + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/SafeChainAgent-windows-arm64.msi", + checksum: + "sha256:5b08dd4749c8befe5379bc01f7a8a5ac1d6a35b6bee37c6c72a4ba8744c3b052", + }, + }, +}; + /** * Builds the download URL for the SafeChain Agent installer. * @param {string} fileName @@ -31,3 +47,67 @@ export async function downloadFile(url, destPath) { export function getAgentVersion() { return ULTIMATE_VERSION; } + +/** + * Returns download info (url, checksum) for the current OS and architecture. + * @returns {{ url: string, checksum: string } | null} + */ +export function getDownloadInfoForCurrentPlatform() { + const platform = process.platform; + const arch = process.arch; + + if (!Object.hasOwn(DOWNLOAD_URLS, platform)) { + return null; + } + const platformUrls = + DOWNLOAD_URLS[/** @type {keyof typeof DOWNLOAD_URLS} */ (platform)]; + + if (!Object.hasOwn(platformUrls, arch)) { + return null; + } + + return platformUrls[/** @type {keyof typeof platformUrls} */ (arch)]; +} + +/** + * Verifies the checksum of a file. + * @param {string} filePath + * @param {string} expectedChecksum - Format: "algorithm:hash" (e.g., "sha256:abc123...") + * @returns {Promise} + */ +async function verifyChecksum(filePath, expectedChecksum) { + const [algorithm, expected] = expectedChecksum.split(":"); + + const hash = createHash(algorithm); + + if (filePath.includes("..")) throw new Error("Invalid file path"); + const stream = createReadStream(filePath); + + for await (const chunk of stream) { + hash.update(chunk); + } + + const actual = hash.digest("hex"); + return actual === expected; +} + +/** + * Downloads the SafeChain agent for the current OS/arch and verifies its checksum. + * @param {string} fileName - Destination file path + * @returns {Promise} The file path if successful, null if no download URL for current platform + */ +export async function downloadAgentToFile(fileName) { + const info = getDownloadInfoForCurrentPlatform(); + if (!info) { + return null; + } + + await downloadFile(info.url, fileName); + + const isValid = await verifyChecksum(fileName, info.checksum); + if (!isValid) { + throw new Error("Checksum verification failed"); + } + + return fileName; +} diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 3cd3428..33ae293 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -1,14 +1,13 @@ -import { arch, tmpdir } from "os"; +import { tmpdir } from "os"; import { unlinkSync } from "fs"; import { join } from "path"; import { execSync } from "child_process"; import { ui } from "../environment/userInteraction.js"; import { safeSpawn } from "../utils/safeSpawn.js"; -import { - getAgentDownloadUrl, - getAgentVersion, - downloadFile, -} from "./downloadAgent.js"; +import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; + +const WINDOWS_SERVICE_NAME = "SafeChainAgent"; +const WINDOWS_APP_NAME = "SafeChain Agent"; export async function installOnWindows() { if (!(await isRunningAsAdmin())) { @@ -19,18 +18,17 @@ export async function installOnWindows() { return; } - const architecture = getWindowsArchitecture(); - const fileName = `SafeChainAgent-windows-${architecture}.msi`; - const downloadUrl = getAgentDownloadUrl(fileName); - const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); + const msiPath = join(tmpdir(), `SafeChainUltimate-${Date.now()}.msi`); ui.emptyLine(); - ui.writeInformation( - `๐Ÿ“ฅ Downloading SafeChain Ultimate ${getAgentVersion()} (${architecture})...`, - ); - ui.writeVerbose(`Download URL: ${downloadUrl}`); + ui.writeInformation(`๐Ÿ“ฅ Downloading SafeChain Ultimate ${getAgentVersion()}`); ui.writeVerbose(`Destination: ${msiPath}`); - await downloadFile(downloadUrl, msiPath); + + const result = await downloadAgentToFile(msiPath); + if (!result) { + ui.writeError("No download available for this platform/architecture."); + return; + } try { ui.emptyLine(); @@ -73,13 +71,6 @@ async function isRunningAsAdmin() { return result.status === 0 && result.stdout.trim() === "True"; } -function getWindowsArchitecture() { - const nodeArch = arch(); - if (nodeArch === "x64") return "amd64"; - if (nodeArch === "arm64") return "arm64"; - throw new Error(`Unsupported architecture: ${nodeArch}`); -} - async function uninstallIfInstalled() { // Query Win32_Product via WMI to find the installed SafeChain Agent. // If found, outputs the product GUID (e.g., "{12345678-1234-...}") needed for msiexec uninstall. @@ -88,7 +79,7 @@ async function uninstallIfInstalled() { let productCode; try { productCode = execSync( - `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`, + `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='${WINDOWS_APP_NAME}'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`, { encoding: "utf8" }, ).trim(); } catch { @@ -132,7 +123,7 @@ async function runMsiInstaller(msiPath) { async function stopServiceIfRunning() { ui.writeInformation("โน๏ธ Stopping running service..."); - const result = await safeSpawn("net", ["stop", "SafeChainAgent"], { + const result = await safeSpawn("net", ["stop", WINDOWS_SERVICE_NAME], { stdio: "pipe", }); @@ -142,8 +133,10 @@ async function stopServiceIfRunning() { } async function startService() { - ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); - const queryResult = await safeSpawn("sc", ["query", "SafeChainAgent"], { + ui.writeVerbose( + `Checking service status: sc query "${WINDOWS_SERVICE_NAME}"`, + ); + const queryResult = await safeSpawn("sc", ["query", WINDOWS_SERVICE_NAME], { stdio: "pipe", }); @@ -152,8 +145,8 @@ async function startService() { return; } - ui.writeVerbose('Running: net start "SafeChainAgent"'); - const startResult = await safeSpawn("net", ["start", "SafeChainAgent"], { + ui.writeVerbose(`Running: net start "${WINDOWS_SERVICE_NAME}"`); + const startResult = await safeSpawn("net", ["start", WINDOWS_SERVICE_NAME], { stdio: "pipe", }); From 626bb0d2b9c4262a92472beda086cbb25fe5f715 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 20 Jan 2026 12:21:45 +0100 Subject: [PATCH 27/46] Don't start the windows service - the msi already does this --- .../src/installation/installOnWindows.js | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 33ae293..2380a7f 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -35,16 +35,9 @@ export async function installOnWindows() { await stopServiceIfRunning(); await uninstallIfInstalled(); - // Wait a moment for uninstall to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); - ui.writeInformation("โš™๏ธ Installing SafeChain Ultimate..."); await runMsiInstaller(msiPath); - ui.emptyLine(); - ui.writeInformation("๐Ÿš€ Starting SafeChain Ultimate service..."); - await startService(); - ui.emptyLine(); ui.writeInformation( "โœ… SafeChain Ultimate installed and started successfully!", @@ -132,31 +125,6 @@ async function stopServiceIfRunning() { } } -async function startService() { - ui.writeVerbose( - `Checking service status: sc query "${WINDOWS_SERVICE_NAME}"`, - ); - const queryResult = await safeSpawn("sc", ["query", WINDOWS_SERVICE_NAME], { - stdio: "pipe", - }); - - if (queryResult.status === 0 && queryResult.stdout.includes("RUNNING")) { - ui.writeVerbose("SafeChain Ultimate service is already running."); - return; - } - - ui.writeVerbose(`Running: net start "${WINDOWS_SERVICE_NAME}"`); - const startResult = await safeSpawn("net", ["start", WINDOWS_SERVICE_NAME], { - stdio: "pipe", - }); - - if (startResult.status !== 0) { - throw new Error( - `Failed to start service (exit code: ${startResult.status})`, - ); - } -} - /** * @param {string} msiPath */ From a7e21bbfe272e31b11c7aae7c7b1e825090c2aa2 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 21 Jan 2026 07:58:06 +0100 Subject: [PATCH 28/46] Update download urls --- .../src/installation/downloadAgent.js | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index 2441f7d..0ee994e 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -8,14 +8,26 @@ const ULTIMATE_VERSION = "v0.2.0"; const DOWNLOAD_URLS = { win32: { x64: { - url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/SafeChainAgent-windows-amd64.msi", + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-windows-amd64.msi", checksum: - "sha256:c699f74a3666d85b70b8ede076a2192a6a023f1b395e8e6c7556927ee698a020", + "sha256:bba5deb250ebc6008f1cb33fa4209d2455a2f47fa99f0a40e3babef64939ac77", }, arm64: { - url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/SafeChainAgent-windows-arm64.msi", + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-windows-arm64.msi", checksum: - "sha256:5b08dd4749c8befe5379bc01f7a8a5ac1d6a35b6bee37c6c72a4ba8744c3b052", + "sha256:9553ed15d5efed4185b990a1b86af0b11c23f11d96f8ce04e16b6b98aaf0506e", + }, + }, + darwin: { + x64: { + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-darwin-amd64.pkg", + checksum: + "sha256:cbccf32e987a45bc8cc20b620f7b597ff7f9c2f966c2bc21132349612ddb619f", + }, + arm64: { + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-darwin-arm64.pkg", + checksum: + "sha256:4d53a43a47bf7e8133eb61d306a1fb16348b9ec89c1c825e5f746f4fe847796e", }, }, }; From d4c496d60d625b1ba6886cd8fd312af79bd2afc7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 21 Jan 2026 09:14:44 +0100 Subject: [PATCH 29/46] Add mac os installation --- .../src/installation/installOnMacOS.js | 78 +++++++++++++++++++ .../src/installation/installUltimate.js | 3 + 2 files changed, 81 insertions(+) create mode 100644 packages/safe-chain/src/installation/installOnMacOS.js diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js new file mode 100644 index 0000000..6963291 --- /dev/null +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -0,0 +1,78 @@ +import { tmpdir } from "os"; +import { unlinkSync } from "fs"; +import { join } from "path"; +import { ui } from "../environment/userInteraction.js"; +import { safeSpawn } from "../utils/safeSpawn.js"; +import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; + +const MACOS_SERVICE_LABEL = "com.aikido.SafeChainAgent"; + +export async function installOnMacOS() { + if (!isRunningAsRoot()) { + ui.writeError("Root privileges required."); + ui.writeInformation("Please run this command with sudo:"); + ui.writeInformation(" sudo safe-chain --ultimate"); + return; + } + + const pkgPath = join(tmpdir(), `SafeChainUltimate-${Date.now()}.pkg`); + + ui.emptyLine(); + ui.writeInformation(`๐Ÿ“ฅ Downloading SafeChain Ultimate ${getAgentVersion()}`); + ui.writeVerbose(`Destination: ${pkgPath}`); + + const result = await downloadAgentToFile(pkgPath); + if (!result) { + ui.writeError("No download available for this platform/architecture."); + return; + } + + try { + ui.writeInformation("โš™๏ธ Installing SafeChain Ultimate..."); + await runPkgInstaller(pkgPath); + + ui.emptyLine(); + ui.writeInformation( + "โœ… SafeChain Ultimate installed and started successfully!", + ); + ui.emptyLine(); + } finally { + ui.writeVerbose(`Cleaning up temporary file: ${pkgPath}`); + cleanup(pkgPath); + } +} + +function isRunningAsRoot() { + const rootUserUid = 0; + return process.getuid?.() === rootUserUid; +} + +/** + * @param {string} pkgPath + */ +async function runPkgInstaller(pkgPath) { + ui.writeVerbose(`Running: installer -pkg "${pkgPath}" -target /`); + + const result = await safeSpawn( + "installer", + ["-pkg", pkgPath, "-target", "/"], + { + stdio: "inherit", + }, + ); + + if (result.status !== 0) { + throw new Error(`PKG installer failed (exit code: ${result.status})`); + } +} + +/** + * @param {string} pkgPath + */ +function cleanup(pkgPath) { + try { + unlinkSync(pkgPath); + } catch { + ui.writeVerbose("Failed to clean up temporary installer file."); + } +} diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 086b6a4..a79a2b1 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -2,6 +2,7 @@ import { platform } from "os"; import { ui } from "../environment/userInteraction.js"; import { initializeCliArguments } from "../config/cliArguments.js"; import { installOnWindows } from "./installOnWindows.js"; +import { installOnMacOS } from "./installOnMacOS.js"; export async function installUltimate() { initializeCliArguments(process.argv); @@ -10,6 +11,8 @@ export async function installUltimate() { if (operatingSystem === "win32") { await installOnWindows(); + } else if (operatingSystem === "darwin") { + await installOnMacOS(); } else { ui.writeInformation( `${operatingSystem} is not supported yet by SafeChain's ultimate version.`, From b9aade2da40082c95c992f02427fc161be95307a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 21 Jan 2026 09:18:26 +0100 Subject: [PATCH 30/46] Remove unused variable --- packages/safe-chain/src/installation/installOnMacOS.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index 6963291..b2a953a 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -5,8 +5,6 @@ import { ui } from "../environment/userInteraction.js"; import { safeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; -const MACOS_SERVICE_LABEL = "com.aikido.SafeChainAgent"; - export async function installOnMacOS() { if (!isRunningAsRoot()) { ui.writeError("Root privileges required."); From 9cde77a408bcfc97f474ec1c213321ec03a4d612 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 22 Jan 2026 08:20:45 +0100 Subject: [PATCH 31/46] PR comments --- .../src/installation/downloadAgent.js | 18 +++++----- .../src/installation/installOnMacOS.js | 10 ++++-- .../src/installation/installOnWindows.js | 34 ++++++++++++++----- packages/safe-chain/src/utils/safeSpawn.js | 16 +++++++++ 4 files changed, 57 insertions(+), 21 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index 0ee994e..2f2baac 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -3,31 +3,31 @@ import { createHash } from "crypto"; import { pipeline } from "stream/promises"; import fetch from "make-fetch-happen"; -const ULTIMATE_VERSION = "v0.2.0"; +const ULTIMATE_VERSION = "v0.2.1"; const DOWNLOAD_URLS = { win32: { x64: { - url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-windows-amd64.msi", + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-windows-amd64.msi`, checksum: - "sha256:bba5deb250ebc6008f1cb33fa4209d2455a2f47fa99f0a40e3babef64939ac77", + "sha256:8d86a44d314746099ba50cfae0cc1eae6232522deb348b226da92aae12754eec", }, arm64: { - url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-windows-arm64.msi", + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-windows-arm64.msi`, checksum: - "sha256:9553ed15d5efed4185b990a1b86af0b11c23f11d96f8ce04e16b6b98aaf0506e", + "sha256:ab5b8335cc257d53424f73d6681920875083cd9b3f53e52d944bf867a415e027", }, }, darwin: { x64: { - url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-darwin-amd64.pkg", + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-darwin-amd64.pkg`, checksum: - "sha256:cbccf32e987a45bc8cc20b620f7b597ff7f9c2f966c2bc21132349612ddb619f", + "sha256:73f83d9352c4fd25f7693d9e53bbbb2b7ac70d16217d745495c9efb50dc4a3a6", }, arm64: { - url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-darwin-arm64.pkg", + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-darwin-arm64.pkg`, checksum: - "sha256:4d53a43a47bf7e8133eb61d306a1fb16348b9ec89c1c825e5f746f4fe847796e", + "sha256:bd419e9c82488539b629b04c97aa1d2dc90e54ff045bd7277a6b40d26f8ebc73", }, }, }; diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index b2a953a..0d7081c 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -2,7 +2,7 @@ import { tmpdir } from "os"; import { unlinkSync } from "fs"; import { join } from "path"; import { ui } from "../environment/userInteraction.js"; -import { safeSpawn } from "../utils/safeSpawn.js"; +import { printVerboseAndSafeSpawn, safeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; export async function installOnMacOS() { @@ -49,9 +49,13 @@ function isRunningAsRoot() { * @param {string} pkgPath */ async function runPkgInstaller(pkgPath) { - ui.writeVerbose(`Running: installer -pkg "${pkgPath}" -target /`); + // Uses installer to install the package (https://ss64.com/mac/installer.html) + // Options: + // -pkg (required): The package to be installed. + // -target (required): The target volume is specified with the -target parameter. + // --> "-target /" installs to the current boot volume. - const result = await safeSpawn( + const result = await printVerboseAndSafeSpawn( "installer", ["-pkg", pkgPath, "-target", "/"], { diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 2380a7f..c6bc744 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -3,7 +3,7 @@ import { unlinkSync } from "fs"; import { join } from "path"; import { execSync } from "child_process"; import { ui } from "../environment/userInteraction.js"; -import { safeSpawn } from "../utils/safeSpawn.js"; +import { printVerboseAndSafeSpawn, safeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; const WINDOWS_SERVICE_NAME = "SafeChainAgent"; @@ -87,7 +87,12 @@ async function uninstallIfInstalled() { ui.writeInformation("๐Ÿ—‘๏ธ Removing previous installation..."); ui.writeVerbose(`Found product code: ${productCode}`); - const uninstallResult = await safeSpawn( + // Use msiexec to run the msi installer quitely (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec) + // Options: + // - /x: Uninstalls the package. + // - /qn: Specifies there's no UI during the installation process. + // - /norestart: Stops the device from restarting after the installation completes. + const uninstallResult = await printVerboseAndSafeSpawn( "msiexec", ["/x", productCode, "/qn", "/norestart"], { stdio: "inherit" }, @@ -102,11 +107,18 @@ async function uninstallIfInstalled() { * @param {string} msiPath */ async function runMsiInstaller(msiPath) { - ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); + // Use msiexec to run the msi installer quitely (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec) + // Options: + // - /i: Specifies normal installation + // - /qn: Specifies there's no UI during the installation process. - const result = await safeSpawn("msiexec", ["/i", msiPath, "/qn"], { - stdio: "inherit", - }); + const result = await printVerboseAndSafeSpawn( + "msiexec", + ["/i", msiPath, "/qn"], + { + stdio: "inherit", + }, + ); if (result.status !== 0) { throw new Error(`MSI installer failed (exit code: ${result.status})`); @@ -116,9 +128,13 @@ async function runMsiInstaller(msiPath) { async function stopServiceIfRunning() { ui.writeInformation("โน๏ธ Stopping running service..."); - const result = await safeSpawn("net", ["stop", WINDOWS_SERVICE_NAME], { - stdio: "pipe", - }); + const result = await printVerboseAndSafeSpawn( + "net", + ["stop", WINDOWS_SERVICE_NAME], + { + stdio: "pipe", + }, + ); if (result.status !== 0) { ui.writeVerbose("Service not running (will start after installation)."); diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index e17bdb5..69c827a 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -1,5 +1,6 @@ import { spawn, execSync } from "child_process"; import os from "os"; +import { ui } from "../environment/userInteraction.js"; /** * @param {string} arg @@ -135,3 +136,18 @@ export async function safeSpawn(command, args, options = {}) { }); }); } + +/** + * @param {string} command + * @param {string[]} args + * @param {import("child_process").SpawnOptions} options + * + * @returns {Promise<{status: number, stdout: string, stderr: string}>} + */ +export async function printVerboseAndSafeSpawn(command, args, options = {}) { + ui.writeVerbose(`Running: ${command} ${args.join(" ")}`); + + const result = await safeSpawn(command, args, options); + + return result; +} From b7a5adf67017b455582eba8688b3fc6aaa8154c7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 22 Jan 2026 09:13:43 +0100 Subject: [PATCH 32/46] Fix linting --- packages/safe-chain/src/installation/installOnMacOS.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index 0d7081c..074bbe6 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -2,7 +2,7 @@ import { tmpdir } from "os"; import { unlinkSync } from "fs"; import { join } from "path"; import { ui } from "../environment/userInteraction.js"; -import { printVerboseAndSafeSpawn, safeSpawn } from "../utils/safeSpawn.js"; +import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; export async function installOnMacOS() { From b2d94aaa167b720c78759221e5ebae7a7d3beb10 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 22 Jan 2026 09:18:23 +0100 Subject: [PATCH 33/46] Fix download links --- packages/safe-chain/src/installation/downloadAgent.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index 2f2baac..df4a933 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -8,24 +8,24 @@ const ULTIMATE_VERSION = "v0.2.1"; const DOWNLOAD_URLS = { win32: { x64: { - url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-windows-amd64.msi`, + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`, checksum: "sha256:8d86a44d314746099ba50cfae0cc1eae6232522deb348b226da92aae12754eec", }, arm64: { - url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-windows-arm64.msi`, + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-arm64.msi`, checksum: "sha256:ab5b8335cc257d53424f73d6681920875083cd9b3f53e52d944bf867a415e027", }, }, darwin: { x64: { - url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-darwin-amd64.pkg`, + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-amd64.pkg`, checksum: "sha256:73f83d9352c4fd25f7693d9e53bbbb2b7ac70d16217d745495c9efb50dc4a3a6", }, arm64: { - url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-darwin-arm64.pkg`, + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-arm64.pkg`, checksum: "sha256:bd419e9c82488539b629b04c97aa1d2dc90e54ff045bd7277a6b40d26f8ebc73", }, From 09730a07752173ec5d10296b59ee0641f417bb56 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 22 Jan 2026 09:23:17 +0100 Subject: [PATCH 34/46] Update application names on Windows --- packages/safe-chain/src/installation/installOnWindows.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index c6bc744..0741fb7 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -6,8 +6,8 @@ import { ui } from "../environment/userInteraction.js"; import { printVerboseAndSafeSpawn, safeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; -const WINDOWS_SERVICE_NAME = "SafeChainAgent"; -const WINDOWS_APP_NAME = "SafeChain Agent"; +const WINDOWS_SERVICE_NAME = "SafeChainUltimate"; +const WINDOWS_APP_NAME = "SafeChain Ultimate"; export async function installOnWindows() { if (!(await isRunningAsAdmin())) { From c02d0785fac707816d10f81efb9dbd5fe06a9cf9 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 22 Jan 2026 11:58:52 +0100 Subject: [PATCH 35/46] Fix tests for mitm registryproxy --- .../registryProxy/registryProxy.mitm.spec.js | 132 ++++++++++++++---- 1 file changed, 105 insertions(+), 27 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js index df4332e..407aa3c 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js @@ -2,12 +2,17 @@ import { before, after, describe, it } from "node:test"; import assert from "node:assert"; import net from "net"; import tls from "tls"; +import { gunzipSync } from "zlib"; import { createSafeChainProxy, mergeSafeChainProxyEnvironmentVariables, } from "./registryProxy.js"; import { getCaCertPath } from "./certUtils.js"; -import { setEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; +import { + setEcoSystem, + ECOSYSTEM_JS, + ECOSYSTEM_PY, +} from "../config/settings.js"; import fs from "fs"; describe("registryProxy.mitm", () => { @@ -33,7 +38,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/lodash" + "/lodash", ); assert.strictEqual(response.statusCode, 200); @@ -45,7 +50,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/lodash/-/lodash-4.17.21.tgz" + "/lodash/-/lodash-4.17.21.tgz", ); // Should get a response (200 or redirect, but not 403 blocked) @@ -57,7 +62,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/this-package-definitely-does-not-exist-12345" + "/this-package-definitely-does-not-exist-12345", ); assert.strictEqual(response.statusCode, 404); @@ -68,7 +73,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/lodash?write=true" + "/lodash?write=true", ); assert.strictEqual(response.statusCode, 200); @@ -79,7 +84,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.yarnpkg.com", - "/lodash" + "/lodash", ); assert.strictEqual(response.statusCode, 200); @@ -90,7 +95,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/lodash" + "/lodash", ); // Check certificate common name matches the target hostname @@ -109,14 +114,14 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/lodash" + "/lodash", ); const { cert: cert2 } = await makeRegistryRequestAndGetCert( proxyHost, proxyPort, "registry.yarnpkg.com", - "/lodash" + "/lodash", ); // Different hostnames should have different certificates @@ -130,14 +135,14 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/lodash" + "/lodash", ); const { cert: cert2 } = await makeRegistryRequestAndGetCert( proxyHost, proxyPort, "registry.npmjs.org", - "/package/lodash" + "/package/lodash", ); // Same hostname should get the same certificate (fingerprint) @@ -159,7 +164,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "pypi.org", - "/packages/source/f/foo_bar/foo_bar-2.0.0.tar.gz" + "/packages/source/f/foo_bar/foo_bar-2.0.0.tar.gz", ); assert.notStrictEqual(response.statusCode, 403); assert.ok(typeof response.body === "string"); @@ -172,7 +177,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "files.pythonhosted.org", - "/packages/xx/yy/foo_bar-2.0.0-py3-none-any.whl" + "/packages/xx/yy/foo_bar-2.0.0-py3-none-any.whl", ); assert.notStrictEqual(response.statusCode, 403); assert.ok(typeof response.body === "string"); @@ -185,7 +190,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "pypi.org", - "/packages/source/f/foo_bar/foo_bar-2.0.0a1.tar.gz" + "/packages/source/f/foo_bar/foo_bar-2.0.0a1.tar.gz", ); assert.notStrictEqual(response.statusCode, 403); assert.ok(typeof response.body === "string"); @@ -198,7 +203,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "pypi.org", - "/packages/source/f/foo_bar/foo_bar-latest.tar.gz" + "/packages/source/f/foo_bar/foo_bar-latest.tar.gz", ); assert.notStrictEqual(response.statusCode, 403); assert.ok(typeof response.body === "string"); @@ -234,34 +239,73 @@ async function makeRegistryRequest(proxyHost, proxyPort, targetHost, path) { }); // Step 4: Send HTTP request over TLS - const httpRequest = `GET ${path} HTTP/1.1\r\nHost: ${targetHost}\r\nConnection: close\r\n\r\n`; + const httpRequest = `GET ${path} HTTP/1.1\r\nHost: ${targetHost}\r\nConnection: close\r\nAccept-encoding: gzip\r\n\r\n`; tlsSocket.write(httpRequest); - // Step 5: Read response + // Step 5: Read response as binary chunks return new Promise((resolve, reject) => { - let data = ""; + const chunks = []; tlsSocket.on("data", (chunk) => { - data += chunk.toString(); + chunks.push(chunk); }); tlsSocket.on("end", () => { - const lines = data.split("\r\n"); - const statusLine = lines[0]; + const buffer = Buffer.concat(chunks); + + // Find the header/body separator (\r\n\r\n) in binary + const separator = Buffer.from("\r\n\r\n"); + let separatorIndex = buffer.indexOf(separator); + if (separatorIndex === -1) { + return reject( + new Error("Invalid HTTP response: no header/body separator"), + ); + } + + // Extract headers as text + const headersText = buffer.subarray(0, separatorIndex).toString("utf8"); + const headerLines = headersText.split("\r\n"); + const statusLine = headerLines[0]; const statusCode = parseInt(statusLine.split(" ")[1]); - // Find body after empty line - const emptyLineIndex = lines.findIndex(line => line === ""); - const body = lines.slice(emptyLineIndex + 1).join("\r\n"); + // Parse headers into object + const headers = {}; + for (let i = 1; i < headerLines.length; i++) { + const colonIndex = headerLines[i].indexOf(":"); + if (colonIndex > 0) { + const key = headerLines[i].substring(0, colonIndex).toLowerCase(); + const value = headerLines[i].substring(colonIndex + 1).trim(); + headers[key] = value; + } + } - resolve({ statusCode, body }); + // Extract body as binary + let bodyBuffer = buffer.subarray(separatorIndex + separator.length); + + // Decode chunked transfer encoding if present + if (headers["transfer-encoding"] === "chunked") { + bodyBuffer = decodeChunked(bodyBuffer); + } + + // Decompress if gzip encoded + if (headers["content-encoding"] === "gzip" && bodyBuffer.length > 0) { + bodyBuffer = gunzipSync(bodyBuffer); + } + + const body = bodyBuffer.toString("utf8"); + resolve({ statusCode, body, headers }); }); tlsSocket.on("error", reject); }); } -async function makeRegistryRequestAndGetCert(proxyHost, proxyPort, targetHost, path) { +async function makeRegistryRequestAndGetCert( + proxyHost, + proxyPort, + targetHost, + path, +) { // Step 1: Connect to proxy const socket = await new Promise((resolve, reject) => { const sock = net.connect({ host: proxyHost, port: proxyPort }, () => { @@ -311,7 +355,7 @@ async function makeRegistryRequestAndGetCert(proxyHost, proxyPort, targetHost, p const statusCode = parseInt(statusLine.split(" ")[1]); // Find body after empty line - const emptyLineIndex = lines.findIndex(line => line === ""); + const emptyLineIndex = lines.findIndex((line) => line === ""); const body = lines.slice(emptyLineIndex + 1).join("\r\n"); resolve({ statusCode, body }); @@ -322,3 +366,37 @@ async function makeRegistryRequestAndGetCert(proxyHost, proxyPort, targetHost, p return { cert: peerCert, response }; } + +/** + * Decode HTTP chunked transfer encoding + * Format: \r\n\r\n ... 0\r\n\r\n + * @param {Buffer} buffer + * @returns {Buffer} + */ +function decodeChunked(buffer) { + const chunks = []; + let offset = 0; + + while (offset < buffer.length) { + // Find the end of the chunk size line + const lineEnd = buffer.indexOf(Buffer.from("\r\n"), offset); + if (lineEnd === -1) break; + + // Parse chunk size (hex) + const sizeHex = buffer.subarray(offset, lineEnd).toString("utf8"); + const chunkSize = parseInt(sizeHex, 16); + + // End of chunks + if (chunkSize === 0) break; + + // Extract chunk data + const dataStart = lineEnd + 2; + const dataEnd = dataStart + chunkSize; + chunks.push(buffer.subarray(dataStart, dataEnd)); + + // Move past chunk data and trailing \r\n + offset = dataEnd + 2; + } + + return Buffer.concat(chunks); +} From f825f84faa88f6e578663674368fc06fe0ccac6e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 22 Jan 2026 12:51:25 +0100 Subject: [PATCH 36/46] Add message about the certificate popup --- .../safe-chain/src/installation/installOnMacOS.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index 074bbe6..0d475b1 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -4,6 +4,7 @@ import { join } from "path"; import { ui } from "../environment/userInteraction.js"; import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; +import chalk from "chalk"; export async function installOnMacOS() { if (!isRunningAsRoot()) { @@ -34,6 +35,17 @@ export async function installOnMacOS() { "โœ… SafeChain Ultimate installed and started successfully!", ); ui.emptyLine(); + ui.writeInformation( + chalk.cyan("๐Ÿ” ") + + chalk.bold("ACTION REQUIRED: ") + + "macOS will show a popup to install our certificate.", + ); + ui.writeInformation( + " " + + chalk.bold("Please accept the certificate") + + " to complete the installation.", + ); + ui.emptyLine(); } finally { ui.writeVerbose(`Cleaning up temporary file: ${pkgPath}`); cleanup(pkgPath); From 309d7df050c636da03ef04757e1f056635710c12 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 27 Jan 2026 07:42:36 +0100 Subject: [PATCH 37/46] Don't insert empty line in rc file when it already ends with an empty line --- packages/safe-chain/src/shell-integration/helpers.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 064aca1..3e71d71 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -99,7 +99,7 @@ export const knownAikidoTools = [ aikidoCommand: "aikido-pipx", ecoSystem: ECOSYSTEM_PY, internalPackageManagerName: "pipx", - } + }, // When adding a new tool here, also update the documentation for the new tool in the README.md ]; @@ -216,7 +216,13 @@ export function addLineToFile(filePath, line, eol) { eol = eol || os.EOL; const fileContent = fs.readFileSync(filePath, "utf-8"); - const updatedContent = fileContent + eol + line + eol; + let updatedContent = fileContent; + + if (!fileContent.endsWith(eol)) { + updatedContent += eol; + } + + updatedContent += line + eol; fs.writeFileSync(filePath, updatedContent, "utf-8"); } From 1058630dd1773b2e2281d5e1e2370d6061d7b0cd Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 27 Jan 2026 11:29:19 +0100 Subject: [PATCH 38/46] Add uninstallation process for ultimate --- packages/safe-chain/bin/safe-chain.js | 14 ++- .../src/installation/installOnMacOS.js | 86 ++++++++++++++++++- .../src/installation/installOnWindows.js | 59 +++++++++++-- .../src/installation/installUltimate.js | 20 ++++- 4 files changed, 167 insertions(+), 12 deletions(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index d048ce1..06add7e 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -16,7 +16,10 @@ import path from "path"; import { fileURLToPath } from "url"; import fs from "fs"; import { knownAikidoTools } from "../src/shell-integration/helpers.js"; -import { installUltimate } from "../src/installation/installUltimate.js"; +import { + installUltimate, + uninstallUltimate, +} from "../src/installation/installUltimate.js"; /** @type {string} */ // This checks the current file's dirname in a way that's compatible with: @@ -67,6 +70,10 @@ if (tool) { (async () => { await installUltimate(); })(); +} else if (command === "--uninstall-ultimate") { + (async () => { + await uninstallUltimate(); + })(); } else if (command === "teardown") { teardownDirectories(); teardown(); @@ -108,6 +115,11 @@ function writeHelp() { "safe-chain --ultimate", )}: This installs the ultimate version of safe-chain, enabling protection for more eco-systems (vscode).`, ); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain --uninstall-ultimate", + )}: This uninstalls the ultimate version of safe-chain.`, + ); ui.writeInformation( `- ${chalk.cyan( "safe-chain teardown", diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index 0d475b1..b2d39ce 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -1,11 +1,14 @@ import { tmpdir } from "os"; -import { unlinkSync } from "fs"; +import { unlinkSync, rmSync } from "fs"; import { join } from "path"; +import { execSync } from "child_process"; import { ui } from "../environment/userInteraction.js"; import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; import chalk from "chalk"; +const MACOS_PKG_IDENTIFIER = "com.aikidosecurity.safechainultimate"; + export async function installOnMacOS() { if (!isRunningAsRoot()) { ui.writeError("Root privileges required."); @@ -52,6 +55,87 @@ export async function installOnMacOS() { } } +export async function uninstallOnMacOS() { + if (!isRunningAsRoot()) { + ui.writeError("Root privileges required."); + ui.writeInformation("Please run this command with sudo:"); + ui.writeInformation(" sudo safe-chain --uninstall-ultimate"); + return; + } + + ui.emptyLine(); + + if (!isPackageInstalled()) { + ui.writeInformation("SafeChain Ultimate is not installed."); + return; + } + + ui.writeInformation("โน๏ธ Stopping service..."); + await stopService(); + + ui.writeInformation("๐Ÿ—‘๏ธ Removing installed files..."); + removeKnownFiles(); + + ui.writeInformation("๐Ÿงน Forgetting package receipt..."); + forgetPackage(); + + ui.emptyLine(); + ui.writeInformation("โœ… SafeChain Ultimate has been uninstalled."); + ui.emptyLine(); +} + +function isPackageInstalled() { + try { + const output = execSync(`pkgutil --pkg-info ${MACOS_PKG_IDENTIFIER}`, { + encoding: "utf8", + stdio: "pipe", + }); + return output.includes(MACOS_PKG_IDENTIFIER); + } catch { + return false; + } +} + +async function stopService() { + const result = await printVerboseAndSafeSpawn( + "launchctl", + ["bootout", `system/${MACOS_PKG_IDENTIFIER}`], + { stdio: "pipe" }, + ); + + if (result.status !== 0) { + ui.writeVerbose("Service not running (will continue with uninstall)."); + } +} + +const MACOS_KNOWN_PATHS = [ + "/Library/Application Support/AikidoSecurity/SafeChainUltimate", + "/Library/Logs/AikidoSecurity/SafeChainUltimate", + `/Library/LaunchDaemons/${MACOS_PKG_IDENTIFIER}.plist`, +]; + +function removeKnownFiles() { + for (const filePath of MACOS_KNOWN_PATHS) { + try { + rmSync(filePath, { recursive: true, force: true }); + ui.writeVerbose(`Removed: ${filePath}`); + } catch { + ui.writeVerbose(`Failed to remove: ${filePath}`); + } + } +} + +function forgetPackage() { + try { + execSync(`pkgutil --forget ${MACOS_PKG_IDENTIFIER}`, { + encoding: "utf8", + stdio: "pipe", + }); + } catch { + ui.writeVerbose("Failed to forget package receipt."); + } +} + function isRunningAsRoot() { const rootUserUid = 0; return process.getuid?.() === rootUserUid; diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 0741fb7..16bf2b7 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -9,6 +9,34 @@ import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; const WINDOWS_SERVICE_NAME = "SafeChainUltimate"; const WINDOWS_APP_NAME = "SafeChain Ultimate"; +export async function uninstallOnWindows() { + if (!(await isRunningAsAdmin())) { + ui.writeError("Administrator privileges required."); + ui.writeInformation( + "Please run this command in an elevated terminal (Run as Administrator).", + ); + return; + } + + ui.emptyLine(); + + const productCode = getInstalledProductCode(); + if (!productCode) { + ui.writeInformation("SafeChain Ultimate is not installed."); + return; + } + + ui.writeInformation("โน๏ธ Stopping running service..."); + await stopServiceIfRunning(); + + ui.writeInformation("๐Ÿ—‘๏ธ Uninstalling SafeChain Ultimate..."); + await uninstallByProductCode(productCode); + + ui.emptyLine(); + ui.writeInformation("โœ… SafeChain Ultimate has been uninstalled."); + ui.emptyLine(); +} + export async function installOnWindows() { if (!(await isRunningAsAdmin())) { ui.writeError("Administrator privileges required."); @@ -64,7 +92,11 @@ async function isRunningAsAdmin() { return result.status === 0 && result.stdout.trim() === "True"; } -async function uninstallIfInstalled() { +/** + * Returns the MSI product code for SafeChain Ultimate, or null if not installed. + * @returns {string | null} + */ +function getInstalledProductCode() { // Query Win32_Product via WMI to find the installed SafeChain Agent. // If found, outputs the product GUID (e.g., "{12345678-1234-...}") needed for msiexec uninstall. ui.writeVerbose(`Finding product code with PowerShell`); @@ -76,15 +108,15 @@ async function uninstallIfInstalled() { { encoding: "utf8" }, ).trim(); } catch { - ui.writeVerbose("No existing installation found (fresh install)."); - return; - } - if (!productCode) { - ui.writeVerbose("No existing installation found (fresh install)."); - return; + return null; } + return productCode || null; +} - ui.writeInformation("๐Ÿ—‘๏ธ Removing previous installation..."); +/** + * @param {string} productCode + */ +async function uninstallByProductCode(productCode) { ui.writeVerbose(`Found product code: ${productCode}`); // Use msiexec to run the msi installer quitely (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec) @@ -103,6 +135,17 @@ async function uninstallIfInstalled() { } } +async function uninstallIfInstalled() { + const productCode = getInstalledProductCode(); + if (!productCode) { + ui.writeVerbose("No existing installation found (fresh install)."); + return; + } + + ui.writeInformation("๐Ÿ—‘๏ธ Removing previous installation..."); + await uninstallByProductCode(productCode); +} + /** * @param {string} msiPath */ diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index a79a2b1..cfcdcca 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -1,8 +1,24 @@ import { platform } from "os"; import { ui } from "../environment/userInteraction.js"; import { initializeCliArguments } from "../config/cliArguments.js"; -import { installOnWindows } from "./installOnWindows.js"; -import { installOnMacOS } from "./installOnMacOS.js"; +import { installOnWindows, uninstallOnWindows } from "./installOnWindows.js"; +import { installOnMacOS, uninstallOnMacOS } from "./installOnMacOS.js"; + +export async function uninstallUltimate() { + initializeCliArguments(process.argv); + + const operatingSystem = platform(); + + if (operatingSystem === "win32") { + await uninstallOnWindows(); + } else if (operatingSystem === "darwin") { + await uninstallOnMacOS(); + } else { + ui.writeInformation( + `Uninstall is not yet supported on ${operatingSystem}.`, + ); + } +} export async function installUltimate() { initializeCliArguments(process.argv); From af4bbb10fcd0b24d168a90680bb8522a88235235 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 27 Jan 2026 12:41:11 +0100 Subject: [PATCH 39/46] Bump safe-chain-internals version --- packages/safe-chain/src/installation/downloadAgent.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index df4a933..4d076ee 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -3,31 +3,31 @@ import { createHash } from "crypto"; import { pipeline } from "stream/promises"; import fetch from "make-fetch-happen"; -const ULTIMATE_VERSION = "v0.2.1"; +const ULTIMATE_VERSION = "v0.2.2"; const DOWNLOAD_URLS = { win32: { x64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`, checksum: - "sha256:8d86a44d314746099ba50cfae0cc1eae6232522deb348b226da92aae12754eec", + "sha256:82d6939579c23c357d0f6d368001a5ac8dc66ce13d32ee1700467555ee97e10a", }, arm64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-arm64.msi`, checksum: - "sha256:ab5b8335cc257d53424f73d6681920875083cd9b3f53e52d944bf867a415e027", + "sha256:d626da40e3d0c4e02a36e6c7e309f18f0ffde64e97a4f2fefd4b25722842ac19", }, }, darwin: { x64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-amd64.pkg`, checksum: - "sha256:73f83d9352c4fd25f7693d9e53bbbb2b7ac70d16217d745495c9efb50dc4a3a6", + "sha256:d7c31914deff8b332bf3d0e18ed00660e47ace87f06f22606c7866f7e0809507", }, arm64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-arm64.pkg`, checksum: - "sha256:bd419e9c82488539b629b04c97aa1d2dc90e54ff045bd7277a6b40d26f8ebc73", + "sha256:73b092689e00c98e3c376afa50fc3477cedfd01445a113d42b36c5fcd956a6f4", }, }, }; From 12caa6d1d43c9d684bee1bcb467300861e12da85 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 27 Jan 2026 12:44:47 +0100 Subject: [PATCH 40/46] Verify download links in a test --- .../src/installation/downloadAgent.js | 4 +- .../src/installation/downloadAgent.spec.js | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 packages/safe-chain/src/installation/downloadAgent.spec.js diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index 4d076ee..cb2f84b 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -5,7 +5,7 @@ import fetch from "make-fetch-happen"; const ULTIMATE_VERSION = "v0.2.2"; -const DOWNLOAD_URLS = { +export const DOWNLOAD_URLS = { win32: { x64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`, @@ -87,7 +87,7 @@ export function getDownloadInfoForCurrentPlatform() { * @param {string} expectedChecksum - Format: "algorithm:hash" (e.g., "sha256:abc123...") * @returns {Promise} */ -async function verifyChecksum(filePath, expectedChecksum) { +export async function verifyChecksum(filePath, expectedChecksum) { const [algorithm, expected] = expectedChecksum.split(":"); const hash = createHash(algorithm); diff --git a/packages/safe-chain/src/installation/downloadAgent.spec.js b/packages/safe-chain/src/installation/downloadAgent.spec.js new file mode 100644 index 0000000..17aecb9 --- /dev/null +++ b/packages/safe-chain/src/installation/downloadAgent.spec.js @@ -0,0 +1,45 @@ +import { describe, it, after } from "node:test"; +import assert from "node:assert"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { unlinkSync } from "node:fs"; +import { + DOWNLOAD_URLS, + downloadFile, + verifyChecksum, +} from "./downloadAgent.js"; + +describe("downloadAgent checksums", { timeout: 120_000 }, () => { + const downloadedFiles = []; + + after(() => { + for (const file of downloadedFiles) { + try { + unlinkSync(file); + } catch { + // ignore cleanup errors + } + } + }); + + for (const [platform, architectures] of Object.entries(DOWNLOAD_URLS)) { + for (const [arch, { url, checksum }] of Object.entries(architectures)) { + it(`${platform}/${arch} checksum matches`, async () => { + const destPath = join( + tmpdir(), + `safe-chain-test-${platform}-${arch}-${Date.now()}` + ); + downloadedFiles.push(destPath); + + await downloadFile(url, destPath); + + const isValid = await verifyChecksum(destPath, checksum); + assert.strictEqual( + isValid, + true, + `Checksum mismatch for ${platform}/${arch} (${url})` + ); + }); + } + } +}); From a016483057a4605e094c94ace3f0f9c1d1c1ec2d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 27 Jan 2026 12:57:40 +0100 Subject: [PATCH 41/46] Remove duplicate "Stopping running service" log --- packages/safe-chain/src/installation/installOnWindows.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 16bf2b7..f20bd9b 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -26,7 +26,6 @@ export async function uninstallOnWindows() { return; } - ui.writeInformation("โน๏ธ Stopping running service..."); await stopServiceIfRunning(); ui.writeInformation("๐Ÿ—‘๏ธ Uninstalling SafeChain Ultimate..."); From 7218d778cfabb98ad2698c6f50fce4c3541d1da5 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 27 Jan 2026 13:06:17 +0100 Subject: [PATCH 42/46] Update commands for ultimate --- packages/safe-chain/bin/safe-chain.js | 44 +++++++++++-------- .../src/installation/installOnMacOS.js | 4 +- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 06add7e..9a07657 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -66,14 +66,17 @@ if (tool) { process.exit(0); } else if (command === "setup") { setup(); -} else if (command === "--ultimate") { - (async () => { - await installUltimate(); - })(); -} else if (command === "--uninstall-ultimate") { - (async () => { - await uninstallUltimate(); - })(); +} else if (command === "ultimate") { + const subCommand = process.argv[3]; + if (subCommand === "uninstall") { + (async () => { + await uninstallUltimate(); + })(); + } else { + (async () => { + await installUltimate(); + })(); + } } else if (command === "teardown") { teardownDirectories(); teardown(); @@ -100,7 +103,7 @@ function writeHelp() { ui.writeInformation( `Available commands: ${chalk.cyan("setup")}, ${chalk.cyan( "teardown", - )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan( + )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("ultimate")}, ${chalk.cyan("help")}, ${chalk.cyan( "--version", )}`, ); @@ -110,16 +113,6 @@ function writeHelp() { "safe-chain setup", )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.`, ); - ui.writeInformation( - `- ${chalk.cyan( - "safe-chain --ultimate", - )}: This installs the ultimate version of safe-chain, enabling protection for more eco-systems (vscode).`, - ); - ui.writeInformation( - `- ${chalk.cyan( - "safe-chain --uninstall-ultimate", - )}: This uninstalls the ultimate version of safe-chain.`, - ); ui.writeInformation( `- ${chalk.cyan( "safe-chain teardown", @@ -136,6 +129,19 @@ function writeHelp() { )}): Display the current version of safe-chain.`, ); ui.emptyLine(); + ui.writeInformation(chalk.bold("Ultimate commands:")); + ui.emptyLine(); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain ultimate", + )}: Install the ultimate version of safe-chain, enabling protection for more eco-systems.`, + ); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain ultimate uninstall", + )}: Uninstall the ultimate version of safe-chain.`, + ); + ui.emptyLine(); } async function getVersion() { diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index b2d39ce..018b911 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -13,7 +13,7 @@ export async function installOnMacOS() { if (!isRunningAsRoot()) { ui.writeError("Root privileges required."); ui.writeInformation("Please run this command with sudo:"); - ui.writeInformation(" sudo safe-chain --ultimate"); + ui.writeInformation(" sudo safe-chain ultimate"); return; } @@ -59,7 +59,7 @@ export async function uninstallOnMacOS() { if (!isRunningAsRoot()) { ui.writeError("Root privileges required."); ui.writeInformation("Please run this command with sudo:"); - ui.writeInformation(" sudo safe-chain --uninstall-ultimate"); + ui.writeInformation(" sudo safe-chain ultimate uninstall"); return; } From a3ab80b8b44cd232bbbeb36c849561a38ce56d59 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 28 Jan 2026 07:53:39 +0100 Subject: [PATCH 43/46] PR comment: extract requireRootPrivileges / requireAdminPrivileges into separate function --- .../src/installation/installOnMacOS.js | 36 ++++++++++++------- .../src/installation/installOnWindows.js | 28 +++++++++------ 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index 018b911..21f8f1d 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -9,11 +9,29 @@ import chalk from "chalk"; const MACOS_PKG_IDENTIFIER = "com.aikidosecurity.safechainultimate"; +/** + * Checks if root privileges are available and displays error message if not. + * @param {string} command - The sudo command to show in the error message + * @returns {boolean} True if running as root, false otherwise. + */ +function requireRootPrivileges(command) { + if (isRunningAsRoot()) { + return true; + } + + ui.writeError("Root privileges required."); + ui.writeInformation("Please run this command with sudo:"); + ui.writeInformation(` ${command}`); + return false; +} + +function isRunningAsRoot() { + const rootUserUid = 0; + return process.getuid?.() === rootUserUid; +} + export async function installOnMacOS() { - if (!isRunningAsRoot()) { - ui.writeError("Root privileges required."); - ui.writeInformation("Please run this command with sudo:"); - ui.writeInformation(" sudo safe-chain ultimate"); + if (!requireRootPrivileges("sudo safe-chain ultimate")) { return; } @@ -56,10 +74,7 @@ export async function installOnMacOS() { } export async function uninstallOnMacOS() { - if (!isRunningAsRoot()) { - ui.writeError("Root privileges required."); - ui.writeInformation("Please run this command with sudo:"); - ui.writeInformation(" sudo safe-chain ultimate uninstall"); + if (!requireRootPrivileges("sudo safe-chain ultimate uninstall")) { return; } @@ -136,11 +151,6 @@ function forgetPackage() { } } -function isRunningAsRoot() { - const rootUserUid = 0; - return process.getuid?.() === rootUserUid; -} - /** * @param {string} pkgPath */ diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index f20bd9b..4cee911 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -10,11 +10,7 @@ const WINDOWS_SERVICE_NAME = "SafeChainUltimate"; const WINDOWS_APP_NAME = "SafeChain Ultimate"; export async function uninstallOnWindows() { - if (!(await isRunningAsAdmin())) { - ui.writeError("Administrator privileges required."); - ui.writeInformation( - "Please run this command in an elevated terminal (Run as Administrator).", - ); + if (!(await requireAdminPrivileges())) { return; } @@ -37,11 +33,7 @@ export async function uninstallOnWindows() { } export async function installOnWindows() { - if (!(await isRunningAsAdmin())) { - ui.writeError("Administrator privileges required."); - ui.writeInformation( - "Please run this command in an elevated terminal (Run as Administrator).", - ); + if (!(await requireAdminPrivileges())) { return; } @@ -76,6 +68,22 @@ export async function installOnWindows() { } } +/** + * Checks if admin privileges are available and displays error message if not. + * @returns {Promise} True if running as admin, false otherwise. + */ +async function requireAdminPrivileges() { + if (await isRunningAsAdmin()) { + return true; + } + + ui.writeError("Administrator privileges required."); + ui.writeInformation( + "Please run this command in an elevated terminal (Run as Administrator).", + ); + return false; +} + async function isRunningAsAdmin() { // Uses Windows Security API to check if current process has admin privileges. // Returns "True" or "False" as a string. From 57c090c3a773857480d997b4995116e2b3324981 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 28 Jan 2026 07:54:35 +0100 Subject: [PATCH 44/46] Rename output --- packages/safe-chain/src/installation/installOnMacOS.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index 21f8f1d..ab20a8a 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -88,7 +88,7 @@ export async function uninstallOnMacOS() { ui.writeInformation("โน๏ธ Stopping service..."); await stopService(); - ui.writeInformation("๐Ÿ—‘๏ธ Removing installed files..."); + ui.writeInformation("๐Ÿ—‘๏ธ Removing files..."); removeKnownFiles(); ui.writeInformation("๐Ÿงน Forgetting package receipt..."); From aa6553716d056fe58a1a2d55fbbb33c98bc1f39f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 28 Jan 2026 15:33:45 +0100 Subject: [PATCH 45/46] Mac: use uninstaller script --- .../src/installation/downloadAgent.js | 10 +-- .../src/installation/installOnMacOS.js | 65 +++++-------------- 2 files changed, 22 insertions(+), 53 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index cb2f84b..a5dcb0d 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -3,31 +3,31 @@ import { createHash } from "crypto"; import { pipeline } from "stream/promises"; import fetch from "make-fetch-happen"; -const ULTIMATE_VERSION = "v0.2.2"; +const ULTIMATE_VERSION = "v0.2.3"; export const DOWNLOAD_URLS = { win32: { x64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`, checksum: - "sha256:82d6939579c23c357d0f6d368001a5ac8dc66ce13d32ee1700467555ee97e10a", + "sha256:bd196ae05b876588f828a57c4d19b3e7ad96ba40007cf2b36693dc6e792d28cc", }, arm64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-arm64.msi`, checksum: - "sha256:d626da40e3d0c4e02a36e6c7e309f18f0ffde64e97a4f2fefd4b25722842ac19", + "sha256:79e046f24405e869494291e77c6d8640c8dc58d2ac1db87d3038e9eb8afbdc8b", }, }, darwin: { x64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-amd64.pkg`, checksum: - "sha256:d7c31914deff8b332bf3d0e18ed00660e47ace87f06f22606c7866f7e0809507", + "sha256:99868cb663eef44d063d995d2dcc063f55b10eb719ee945d05fe8cf5fef5e2a5", }, arm64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-arm64.pkg`, checksum: - "sha256:73b092689e00c98e3c376afa50fc3477cedfd01445a113d42b36c5fcd956a6f4", + "sha256:000b334c2eb85d8692be5d23af73f8b9fb686c9db726992223187b341ea79306", }, }, }; diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index ab20a8a..ae4fea3 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -1,7 +1,7 @@ import { tmpdir } from "os"; -import { unlinkSync, rmSync } from "fs"; +import { unlinkSync } from "fs"; import { join } from "path"; -import { execSync } from "child_process"; +import { execSync, spawnSync } from "child_process"; import { ui } from "../environment/userInteraction.js"; import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; @@ -73,6 +73,9 @@ export async function installOnMacOS() { } } +const MACOS_UNINSTALL_SCRIPT = + "/Library/Application Support/AikidoSecurity/SafeChainUltimate/scripts/uninstall"; + export async function uninstallOnMacOS() { if (!requireRootPrivileges("sudo safe-chain ultimate uninstall")) { return; @@ -85,14 +88,20 @@ export async function uninstallOnMacOS() { return; } - ui.writeInformation("โน๏ธ Stopping service..."); - await stopService(); + ui.writeInformation("๐Ÿ—‘๏ธ Uninstalling SafeChain Ultimate..."); + ui.writeVerbose(`Running: ${MACOS_UNINSTALL_SCRIPT}`); - ui.writeInformation("๐Ÿ—‘๏ธ Removing files..."); - removeKnownFiles(); + const result = spawnSync(MACOS_UNINSTALL_SCRIPT, { + stdio: "inherit", + shell: true, + }); - ui.writeInformation("๐Ÿงน Forgetting package receipt..."); - forgetPackage(); + if (result.status !== 0) { + ui.writeError( + `Uninstall script failed (exit code: ${result.status}). Please try again or remove manually.`, + ); + return; + } ui.emptyLine(); ui.writeInformation("โœ… SafeChain Ultimate has been uninstalled."); @@ -111,46 +120,6 @@ function isPackageInstalled() { } } -async function stopService() { - const result = await printVerboseAndSafeSpawn( - "launchctl", - ["bootout", `system/${MACOS_PKG_IDENTIFIER}`], - { stdio: "pipe" }, - ); - - if (result.status !== 0) { - ui.writeVerbose("Service not running (will continue with uninstall)."); - } -} - -const MACOS_KNOWN_PATHS = [ - "/Library/Application Support/AikidoSecurity/SafeChainUltimate", - "/Library/Logs/AikidoSecurity/SafeChainUltimate", - `/Library/LaunchDaemons/${MACOS_PKG_IDENTIFIER}.plist`, -]; - -function removeKnownFiles() { - for (const filePath of MACOS_KNOWN_PATHS) { - try { - rmSync(filePath, { recursive: true, force: true }); - ui.writeVerbose(`Removed: ${filePath}`); - } catch { - ui.writeVerbose(`Failed to remove: ${filePath}`); - } - } -} - -function forgetPackage() { - try { - execSync(`pkgutil --forget ${MACOS_PKG_IDENTIFIER}`, { - encoding: "utf8", - stdio: "pipe", - }); - } catch { - ui.writeVerbose("Failed to forget package receipt."); - } -} - /** * @param {string} pkgPath */ From e36b7e80b427b55d2444cb704894c7fd9dfe577c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 28 Jan 2026 15:42:15 +0100 Subject: [PATCH 46/46] Fix uninstall script --- packages/safe-chain/src/installation/installOnMacOS.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index ae4fea3..22ce1a8 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -74,7 +74,7 @@ export async function installOnMacOS() { } const MACOS_UNINSTALL_SCRIPT = - "/Library/Application Support/AikidoSecurity/SafeChainUltimate/scripts/uninstall"; + "/Library/Application\\ Support/AikidoSecurity/SafeChainUltimate/scripts/uninstall"; export async function uninstallOnMacOS() { if (!requireRootPrivileges("sudo safe-chain ultimate uninstall")) {