diff --git a/installer/.gitignore b/installer/.gitignore
new file mode 100644
index 0000000..cbf6040
--- /dev/null
+++ b/installer/.gitignore
@@ -0,0 +1,18 @@
+# Installer build artifacts
+dist/
+node_modules/
+
+# macOS specific
+*.pkg
+*.dmg
+.DS_Store
+
+# Temporary certificate files during development
+*.pem
+*.key
+certs/
+
+# Build logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/installer/build.js b/installer/build.js
new file mode 100644
index 0000000..86b02ea
--- /dev/null
+++ b/installer/build.js
@@ -0,0 +1,254 @@
+#!/usr/bin/env node
+
+/**
+ * Build script for creating standalone Safe Chain binaries
+ * Uses esbuild to bundle ES modules, then @yao-pkg/pkg to create executable
+ */
+
+import { exec } from 'node:child_process';
+import { promisify } from 'node:util';
+import { existsSync, mkdirSync, copyFileSync, writeFileSync, rmSync, readFileSync, readdirSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import * as esbuild from 'esbuild';
+
+const execAsync = promisify(exec);
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const ROOT_DIR = join(__dirname, '..');
+const DIST_DIR = join(__dirname, 'dist');
+const BUNDLE_DIR = join(DIST_DIR, 'bundle');
+const SAFE_CHAIN_DIR = join(ROOT_DIR, 'packages/safe-chain');
+
+/**
+ * Parse command line arguments
+ */
+function parseArgs() {
+ const args = process.argv.slice(2);
+ const platform = args.find(arg => arg.startsWith('--platform='))?.split('=')[1] || 'macos';
+ return { platform };
+}
+
+/**
+ * Ensure dist directory exists
+ */
+function ensureDistDirectory() {
+ if (!existsSync(DIST_DIR)) {
+ mkdirSync(DIST_DIR, { recursive: true });
+ }
+ if (!existsSync(BUNDLE_DIR)) {
+ mkdirSync(BUNDLE_DIR, { recursive: true });
+ }
+}
+
+/**
+ * Bundle ES modules using esbuild
+ * This converts ES modules to CommonJS that pkg can handle
+ */
+async function bundleWithEsbuild() {
+ console.log('Bundling with esbuild...');
+
+ const entryPoint = join(SAFE_CHAIN_DIR, 'bin/safe-chain.js');
+ const outputFile = join(BUNDLE_DIR, 'safe-chain-bundled.cjs');
+
+ await esbuild.build({
+ entryPoints: [entryPoint],
+ bundle: true,
+ platform: 'node',
+ target: 'node20',
+ format: 'cjs',
+ outfile: outputFile,
+ external: [
+ // Keep node: protocol imports external
+ 'node:*',
+ ],
+ loader: {
+ '.json': 'json', // Inline JSON files
+ },
+ banner: {
+ js: `// Polyfill for import.meta.url in CommonJS
+var __filename = __filename || (() => {
+ try {
+ return require('url').fileURLToPath(__filename);
+ } catch (e) {
+ return __filename;
+ }
+})();
+var __dirname = __dirname || require('path').dirname(__filename);
+var import_meta_url = typeof __filename !== 'undefined' ? require('url').pathToFileURL(__filename).href : undefined;
+`,
+ },
+ define: {
+ 'import.meta.url': 'import_meta_url',
+ },
+ minify: false, // Keep readable for debugging
+ sourcemap: false,
+ });
+
+ console.log('✓ Bundle created at:', outputFile);
+ return outputFile;
+}
+
+/**
+ * Build macOS binary and installer
+ */
+async function buildMacOS() {
+ console.log('Building macOS binary...');
+
+ // Step 1: Bundle with esbuild
+ const bundledFile = await bundleWithEsbuild();
+
+ // Step 2: Package with pkg
+ const targetPlatform = 'node20-macos-arm64';
+ const outputPath = join(DIST_DIR, 'safe-chain-macos-arm64');
+
+ // Copy shell-integration files to a staging directory with the structure we want in /snapshot
+ const stagingDir = join(DIST_DIR, 'staging');
+ const stagingShellInt = join(stagingDir, 'src/shell-integration');
+
+ // Clean and create staging directory
+ if (existsSync(stagingDir)) {
+ rmSync(stagingDir, { recursive: true });
+ }
+ mkdirSync(stagingShellInt, { recursive: true });
+
+ // Copy shell-integration directory
+ const shellIntegrationSrc = join(SAFE_CHAIN_DIR, 'src/shell-integration');
+
+ // Helper to copy directory recursively
+ const copyDir = (src, dest) => {
+ if (!existsSync(dest)) {
+ mkdirSync(dest, { recursive: true });
+ }
+ const entries = readdirSync(src, { withFileTypes: true });
+ for (const entry of entries) {
+ const srcPath = join(src, entry.name);
+ const destPath = join(dest, entry.name);
+ if (entry.isDirectory()) {
+ copyDir(srcPath, destPath);
+ } else {
+ copyFileSync(srcPath, destPath);
+ }
+ }
+ };
+
+ copyDir(shellIntegrationSrc, stagingShellInt);
+
+ const pkgArgs = [
+ bundledFile,
+ '--target', targetPlatform,
+ '--output', outputPath,
+ '--compress', 'GZip',
+ // Include contents of staging/src - files will be at /snapshot/src/shell-integration/
+ `--assets=${join(stagingDir, 'src')}/**/*`,
+ ];
+
+ console.log(`Running: npx @yao-pkg/pkg ${pkgArgs.join(' ')}`);
+
+ // Use spawn instead of execFile to avoid issues with glob expansion
+ const { spawn } = await import('node:child_process');
+
+ const pkgProcess = spawn('npx', ['@yao-pkg/pkg', ...pkgArgs], {
+ cwd: __dirname,
+ stdio: 'inherit'
+ });
+
+ await new Promise((resolve, reject) => {
+ pkgProcess.on('close', (code) => {
+ if (code !== 0) {
+ reject(new Error(`pkg failed with code ${code}`));
+ } else {
+ resolve();
+ }
+ });
+ }); console.log('✓ Binary created at:', outputPath);
+
+ // Create installer package
+ await createMacOSInstaller(outputPath);
+}
+
+/**
+ * Create macOS installer with certificate installation
+ */
+async function createMacOSInstaller(binaryPath) {
+ console.log('Creating macOS installer package...');
+
+ const installerDir = join(DIST_DIR, 'macos-installer');
+ const scriptsDir = join(installerDir, 'scripts');
+ const resourcesDir = join(installerDir, 'resources');
+
+ // Create directory structure
+ mkdirSync(scriptsDir, { recursive: true });
+ mkdirSync(resourcesDir, { recursive: true });
+
+ // Copy binary to resources
+ const binaryDestination = join(resourcesDir, 'safe-chain');
+ copyFileSync(binaryPath, binaryDestination);
+
+ // Read installer scripts from separate files
+ const scriptsSourceDir = join(__dirname, 'scripts');
+ const preinstallScript = readFileSync(join(scriptsSourceDir, 'darwin_preinstall.sh'), 'utf8');
+ const postinstallScript = readFileSync(join(scriptsSourceDir, 'darwin_postinstall.sh'), 'utf8');
+ const uninstallScript = readFileSync(join(scriptsSourceDir, 'darwin_uninstall.sh'), 'utf8');
+
+ // Write scripts to installer directory
+ writeFileSync(join(scriptsDir, 'preinstall'), preinstallScript, { mode: 0o755 });
+ writeFileSync(join(scriptsDir, 'postinstall'), postinstallScript, { mode: 0o755 });
+ writeFileSync(join(installerDir, 'uninstall.sh'), uninstallScript, { mode: 0o755 });
+
+ console.log('✓ macOS installer package created at:', installerDir);
+ console.log('');
+ console.log('To create a .pkg installer, run:');
+ console.log(` cd ${installerDir}`);
+ console.log(' pkgbuild --root resources --scripts scripts --identifier com.aikido.safe-chain --version 1.0.0 --install-location /tmp/safe-chain-install SafeChain.pkg');
+}
+
+/**
+ * Build Linux binary and installer
+ */
+async function buildLinux() {
+ // TODO: Implement Linux binary creation
+}
+
+/**
+ * Build Windows binary and installer
+ */
+async function buildWindows() {
+ // TODO: Implement Windows binary creation
+}
+
+/**
+ * Main build function
+ */
+async function build() {
+ const { platform } = parseArgs();
+
+ console.log('=== Safe Chain Installer Builder ===');
+ console.log(`Platform: ${platform}`);
+ console.log('');
+
+ ensureDistDirectory();
+
+ try {
+ switch (platform) {
+ case 'macos':
+ await buildMacOS();
+ break;
+ case 'linux':
+ await buildLinux();
+ break;
+ case 'windows':
+ await buildWindows();
+ break;
+ default:
+ console.error(`Unknown platform: ${platform}`);
+ console.error('Valid options: macos, linux, windows');
+ process.exit(1);
+ }
+ } catch (error) {
+ process.exit(1);
+ }
+}
+
+// Run the build
+build();
diff --git a/installer/generate-certs.js b/installer/generate-certs.js
new file mode 100644
index 0000000..440a542
--- /dev/null
+++ b/installer/generate-certs.js
@@ -0,0 +1,46 @@
+#!/usr/bin/env node
+
+/**
+ * Wrapper script for certificate generation during installer build
+ * This re-exports the certificate generation functionality from the main package
+ */
+
+import { generateCACertificate } from '../packages/safe-chain/src/registryProxy/certUtils.js';
+import { writeFileSync, mkdirSync } from 'node:fs';
+import { join } from 'node:path';
+
+/**
+ * Generate certificate files (simple version for installer build)
+ * For the full CLI version with nice output, use: safe-chain generate-cert
+ *
+ * @param {string} outputDir - Directory to save certificate files
+ * @returns {Promise<{certPath: string, keyPath: string}>}
+ */
+export async function generateCertificates(outputDir) {
+ console.log('Generating Safe Chain CA certificate...');
+
+ mkdirSync(outputDir, { recursive: true });
+
+ const { cert, key } = generateCACertificate();
+
+ const certPath = join(outputDir, 'ca-cert.pem');
+ const keyPath = join(outputDir, 'ca-key.pem');
+
+ writeFileSync(certPath, cert);
+ writeFileSync(keyPath, key);
+
+ console.log('✓ Certificate generated:');
+ console.log(` Certificate: ${certPath}`);
+ console.log(` Private Key: ${keyPath}`);
+
+ return { certPath, keyPath };
+}
+
+// CLI usage - when run directly
+if (import.meta.url === `file://${process.argv[1]}`) {
+ const outputDir = process.argv[2] || './certs';
+ generateCertificates(outputDir).catch(error => {
+ console.error('Error generating certificates:', error);
+ process.exit(1);
+ });
+}
diff --git a/installer/package.json b/installer/package.json
new file mode 100644
index 0000000..bcc6cfb
--- /dev/null
+++ b/installer/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@aikidosec/safe-chain-installer",
+ "version": "1.0.0",
+ "description": "Installer for Aikido Safe Chain - creates standalone binaries and installs system certificates",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "node build.js",
+ "build:macos": "node build.js --platform=macos",
+ "build:linux": "node build.js --platform=linux",
+ "build:windows": "node build.js --platform=windows",
+ "build:all": "node build.js --platform=all"
+ },
+ "keywords": [],
+ "author": "Aikido Security",
+ "license": "AGPL-3.0-or-later",
+ "dependencies": {
+ "@yao-pkg/pkg": "^5.15.0",
+ "esbuild": "^0.24.0"
+ },
+ "devDependencies": {}
+}
diff --git a/installer/scripts/darwin_postinstall.sh b/installer/scripts/darwin_postinstall.sh
new file mode 100644
index 0000000..54ae030
--- /dev/null
+++ b/installer/scripts/darwin_postinstall.sh
@@ -0,0 +1,128 @@
+#!/bin/bash
+set -e
+
+echo "Installing Safe Chain..."
+
+# The binary is installed to the location specified by --install-location
+# which is passed as $3 (installation volume/mountpoint)
+INSTALL_LOCATION="${3}/tmp/safe-chain-install"
+
+# Install binary
+mkdir -p /usr/local/bin
+cp "${INSTALL_LOCATION}/safe-chain" /usr/local/bin/safe-chain
+chmod +x /usr/local/bin/safe-chain
+
+# Setup certificate directory in user's home
+# The proxy will use ~/.safe-chain/certs/ so we need to ensure it exists
+# and install the certificate from there
+# Get the actual user (not root) who invoked the installer
+# When using 'installer' command, SUDO_USER is not set, so we use the console user
+ACTUAL_USER=$(stat -f '%Su' /dev/console)
+
+# Get the home directory of the actual user
+USER_HOME=$(eval echo "~${ACTUAL_USER}")
+
+CERT_DIR="${USER_HOME}/.safe-chain/certs"
+mkdir -p "${CERT_DIR}"
+# Set ownership immediately after creating directory
+chown -R "${ACTUAL_USER}:staff" "${USER_HOME}/.safe-chain"
+
+# Generate certificate if it doesn't exist
+# This ensures the same cert is used by both the proxy and system trust store
+if [ ! -f "${CERT_DIR}/ca-cert.pem" ]; then
+ echo "Generating Safe Chain CA certificate..."
+ # Run as the actual user with their HOME set, not root
+ sudo -u "${ACTUAL_USER}" HOME="${USER_HOME}" /usr/local/bin/safe-chain generate-cert --output "${CERT_DIR}"
+fi
+
+# Set correct ownership (important since installer runs as root)
+# Do this AFTER generating certificates so they get the right ownership too
+chown -R "${ACTUAL_USER}:staff" "${USER_HOME}/.safe-chain"
+
+# Install certificate in system trust store
+echo "Installing Safe Chain CA certificate in system trust store..."
+if [ -f "${CERT_DIR}/ca-cert.pem" ]; then
+ security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${CERT_DIR}/ca-cert.pem" || true
+ echo "✓ Certificate installed in system trust store"
+else
+ echo "⚠ Warning: Could not find certificate to install"
+ exit 1
+fi
+
+# Start safe-chain as a background service
+echo "Starting Safe Chain proxy service..."
+
+# Create LaunchAgent for auto-start on login
+LAUNCH_AGENT_DIR="${USER_HOME}/Library/LaunchAgents"
+mkdir -p "${LAUNCH_AGENT_DIR}"
+
+PLIST_PATH="${LAUNCH_AGENT_DIR}/com.aikido.safe-chain.plist"
+cat > "${PLIST_PATH}" << EOF
+
+
+
+
+ Label
+ com.aikido.safe-chain
+ ProgramArguments
+
+ /usr/local/bin/safe-chain
+ run
+
+ EnvironmentVariables
+
+ HTTPS_PROXY
+ http://localhost:8080
+ GLOBAL_AGENT_HTTP_PROXY
+ http://localhost:8080
+ NODE_EXTRA_CA_CERTS
+ ${CERT_DIR}/ca-cert.pem
+
+ RunAtLoad
+
+ KeepAlive
+
+ StandardOutPath
+ ${USER_HOME}/.safe-chain/safe-chain.log
+ StandardErrorPath
+ ${USER_HOME}/.safe-chain/safe-chain.error.log
+
+
+EOF
+
+# Set correct ownership for plist
+chown "${ACTUAL_USER}:staff" "${PLIST_PATH}"
+
+# Set correct ownership for plist
+chown "${ACTUAL_USER}:staff" "${PLIST_PATH}"
+
+# Load the LaunchAgent to start the service now
+# Need to run as the actual user, not root
+sudo -u "${ACTUAL_USER}" launchctl load "${PLIST_PATH}" 2>/dev/null || true
+
+# Give it a moment to start
+sleep 2
+
+# Set system-wide environment variables so all processes can use the proxy
+# These affect all processes for the user, not just the LaunchAgent
+echo "Setting system-wide proxy environment variables..."
+sudo -u "${ACTUAL_USER}" launchctl setenv HTTPS_PROXY "http://localhost:8080"
+sudo -u "${ACTUAL_USER}" launchctl setenv GLOBAL_AGENT_HTTP_PROXY "http://localhost:8080"
+sudo -u "${ACTUAL_USER}" launchctl setenv NODE_EXTRA_CA_CERTS "${CERT_DIR}/ca-cert.pem"
+
+echo "✓ Safe Chain installed successfully!"
+echo ""
+echo "Safe Chain is now running as a background service."
+echo "It will automatically start on login."
+echo ""
+echo "Logs are available at:"
+echo " ${USER_HOME}/.safe-chain/safe-chain.log"
+echo ""
+echo "To manually control the service:"
+echo " Stop: launchctl unload ~/Library/LaunchAgents/com.aikido.safe-chain.plist"
+echo " Start: launchctl load ~/Library/LaunchAgents/com.aikido.safe-chain.plist"
+echo ""
+echo "You can now use npm, pip, yarn without any additional configuration!"
+echo "Package installations will be automatically scanned for malware."
+
+exit 0
diff --git a/installer/scripts/darwin_preinstall.sh b/installer/scripts/darwin_preinstall.sh
new file mode 100644
index 0000000..b38dec2
--- /dev/null
+++ b/installer/scripts/darwin_preinstall.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+set -e
+
+echo "Preparing to install Safe Chain..."
+
+USER_HOME="${HOME}"
+if [ -z "${USER_HOME}" ]; then
+ USER_HOME=~
+fi
+
+# Stop existing service if running
+PLIST_PATH="${USER_HOME}/Library/LaunchAgents/com.aikido.safe-chain.plist"
+if [ -f "${PLIST_PATH}" ]; then
+ echo "Stopping existing Safe Chain service..."
+ launchctl unload "${PLIST_PATH}" 2>/dev/null || true
+fi
+
+# Clear any existing environment variables from previous installation
+launchctl unsetenv HTTPS_PROXY 2>/dev/null || true
+launchctl unsetenv GLOBAL_AGENT_HTTP_PROXY 2>/dev/null || true
+launchctl unsetenv NODE_EXTRA_CA_CERTS 2>/dev/null || true
+
+# Remove old binary if exists
+if [ -f /usr/local/bin/safe-chain ]; then
+ rm -f /usr/local/bin/safe-chain
+fi
+
+exit 0
diff --git a/installer/scripts/darwin_uninstall.sh b/installer/scripts/darwin_uninstall.sh
new file mode 100644
index 0000000..79ab745
--- /dev/null
+++ b/installer/scripts/darwin_uninstall.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+set -e
+
+echo "Uninstalling Safe Chain..."
+
+USER_HOME="${HOME}"
+if [ -z "${USER_HOME}" ]; then
+ USER_HOME=~
+fi
+
+# Stop and unload the LaunchAgent
+PLIST_PATH="${USER_HOME}/Library/LaunchAgents/com.aikido.safe-chain.plist"
+if [ -f "${PLIST_PATH}" ]; then
+ echo "Stopping Safe Chain service..."
+ launchctl unload "${PLIST_PATH}" 2>/dev/null || true
+ rm -f "${PLIST_PATH}"
+fi
+
+# Remove system-wide environment variables
+echo "Removing proxy environment variables..."
+launchctl unsetenv HTTPS_PROXY 2>/dev/null || true
+launchctl unsetenv GLOBAL_AGENT_HTTP_PROXY 2>/dev/null || true
+launchctl unsetenv NODE_EXTRA_CA_CERTS 2>/dev/null || true
+
+# Remove binary
+rm -f /usr/local/bin/safe-chain
+
+# Remove certificate from system keychain
+CERT_PATH="${USER_HOME}/.safe-chain/certs/ca-cert.pem"
+if [ -f "${CERT_PATH}" ]; then
+ echo "Removing certificate from system trust store..."
+ # Find and delete the certificate by common name
+ security delete-certificate -c "safe-chain proxy" /Library/Keychains/System.keychain 2>/dev/null || true
+fi
+
+# Optionally remove the .safe-chain directory (commented out to preserve user data)
+# echo "Remove ~/.safe-chain directory? (y/N)"
+# read -r response
+# if [[ "$response" =~ ^[Yy]$ ]]; then
+# rm -rf "${USER_HOME}/.safe-chain"
+# echo "✓ Configuration and certificates removed"
+# fi
+
+echo "✓ Safe Chain uninstalled successfully!"
+echo ""
+echo "Note: Certificate and configuration files in ~/.safe-chain were preserved."
+echo "To remove them manually: rm -rf ~/.safe-chain"
+
+exit 0
diff --git a/package.json b/package.json
index 6a5dec3..332a680 100644
--- a/package.json
+++ b/package.json
@@ -4,13 +4,15 @@
"type": "module",
"workspaces": [
"packages/*",
- "test/e2e"
+ "test/e2e",
+ "installer"
],
"scripts": {
"test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun",
"test:e2e": "npm run test --workspace=test/e2e",
"lint": "npm run lint --workspace=packages/safe-chain",
- "typecheck": "npm run typecheck --workspace=packages/safe-chain"
+ "typecheck": "npm run typecheck --workspace=packages/safe-chain",
+ "build:installer": "npm run build --workspace=installer"
},
"repository": {
"type": "git",
diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js
index 26f276a..011e460 100755
--- a/packages/safe-chain/bin/safe-chain.js
+++ b/packages/safe-chain/bin/safe-chain.js
@@ -2,11 +2,15 @@
import chalk from "chalk";
import { createRequire } from "module";
+import { readFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { dirname, join } from "node:path";
import { ui } from "../src/environment/userInteraction.js";
import { setup } from "../src/shell-integration/setup.js";
import { teardown } from "../src/shell-integration/teardown.js";
import { setupCi } from "../src/shell-integration/setup-ci.js";
import { runCommand } from "../src/agent/runCommand.js";
+import { generateCertCommand } from "../src/agent/generateCert.js";
if (process.argv.length < 3) {
ui.writeError("No command provided. Please provide a command to execute.");
@@ -32,6 +36,10 @@ if (command === "setup") {
// Pass remaining arguments to runCommand
const runArgs = process.argv.slice(3);
runCommand(runArgs);
+} else if (command === "generate-cert") {
+ // Pass remaining arguments to generateCertCommand
+ const certArgs = process.argv.slice(3);
+ generateCertCommand(certArgs);
} else if (command === "--version" || command === "-v" || command === "-v") {
ui.writeInformation(`Current safe-chain version: ${getVersion()}`);
} else {
@@ -52,8 +60,8 @@ function writeHelp() {
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
"teardown"
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("run")}, ${chalk.cyan(
- "help"
- )}, ${chalk.cyan("--version")}`
+ "generate-cert"
+ )}, ${chalk.cyan("help")}, ${chalk.cyan("--version")}`
);
ui.emptyLine();
ui.writeInformation(
@@ -74,7 +82,12 @@ function writeHelp() {
ui.writeInformation(
`- ${chalk.cyan(
"safe-chain run"
- )}: Run the proxy as a standalone service. Options: --all (default), --js, --py, --ecosystem=`
+ )}: Run the proxy as a standalone service. Sets system-wide proxy environment variables. Options: --verbose`
+ );
+ ui.writeInformation(
+ `- ${chalk.cyan(
+ "safe-chain generate-cert"
+ )}: Generate CA certificate for MITM proxy. Options: --output `
);
ui.writeInformation(
`- ${chalk.cyan(
@@ -85,7 +98,15 @@ function writeHelp() {
}
function getVersion() {
- const require = createRequire(import.meta.url);
- const packageJson = require("../package.json");
- return packageJson.version;
+ try {
+ // Try to load package.json from the expected location
+ const __filename = fileURLToPath(import.meta.url);
+ const __dirname = dirname(__filename);
+ const packageJsonPath = join(__dirname, '../package.json');
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
+ return packageJson.version;
+ } catch (error) {
+ // Fallback for bundled version
+ return '1.0.0';
+ }
}
diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json
index 93a8fd9..f414bde 100644
--- a/packages/safe-chain/package.json
+++ b/packages/safe-chain/package.json
@@ -64,5 +64,18 @@
"type": "git",
"url": "git+https://github.com/AikidoSec/safe-chain.git",
"directory": "packages/safe-chain"
+ },
+ "pkg": {
+ "assets": [
+ "src/**/*.js",
+ "node_modules/**/*"
+ ],
+ "targets": [
+ "node20-macos-arm64",
+ "node20-macos-x64",
+ "node20-linux-x64",
+ "node20-linux-arm64",
+ "node20-win-x64"
+ ]
}
}
diff --git a/packages/safe-chain/src/agent/generateCert.js b/packages/safe-chain/src/agent/generateCert.js
new file mode 100644
index 0000000..08b5d43
--- /dev/null
+++ b/packages/safe-chain/src/agent/generateCert.js
@@ -0,0 +1,60 @@
+/**
+ * Generate certificate command for Safe Chain
+ * Creates CA certificate and key for MITM proxy
+ */
+
+import { generateCACertificate } from "../registryProxy/certUtils.js";
+import { writeFileSync, mkdirSync } from "node:fs";
+import { join } from "node:path";
+import { homedir } from "node:os";
+import { ui } from "../environment/userInteraction.js";
+import chalk from "chalk";
+
+/**
+ * Generate certificate command
+ * @param {string[]} args - Command line arguments
+ */
+export async function generateCertCommand(args) {
+ // Parse output directory from --output flag
+ let outputDir = join(homedir(), ".safe-chain");
+
+ for (let i = 0; i < args.length; i++) {
+ if (args[i] === "--output" && args[i + 1]) {
+ outputDir = args[i + 1];
+ break;
+ }
+ }
+
+ try {
+ ui.writeInformation(chalk.bold("Generating Safe Chain CA certificate..."));
+ ui.emptyLine();
+
+ // Create output directory
+ mkdirSync(outputDir, { recursive: true });
+
+ // Generate certificate
+ const { cert, key } = generateCACertificate();
+
+ // Write certificate and key files
+ const certPath = join(outputDir, "ca-cert.pem");
+ const keyPath = join(outputDir, "ca-key.pem");
+
+ writeFileSync(certPath, cert);
+ writeFileSync(keyPath, key);
+
+ ui.writeInformation(chalk.green("✓") + " Certificate generated successfully!");
+ ui.emptyLine();
+ ui.writeInformation(chalk.bold("Files created:"));
+ ui.writeInformation(` Certificate: ${chalk.cyan(certPath)}`);
+ ui.writeInformation(` Private Key: ${chalk.cyan(keyPath)}`);
+ ui.emptyLine();
+ ui.writeInformation(chalk.dim("To install this certificate in your system trust store:"));
+ ui.writeInformation(chalk.dim(" macOS: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain " + certPath));
+ ui.writeInformation(chalk.dim(" Linux: sudo cp " + certPath + " /usr/local/share/ca-certificates/ && sudo update-ca-certificates"));
+ ui.writeInformation(chalk.dim(" Windows: certutil -addstore -f ROOT " + certPath));
+ ui.emptyLine();
+ } catch (/** @type {any} */ error) {
+ ui.writeError(`Failed to generate certificate: ${error.message}`);
+ process.exit(1);
+ }
+}
diff --git a/packages/safe-chain/src/agent/runCommand.js b/packages/safe-chain/src/agent/runCommand.js
index 5855c5f..ac7b266 100644
--- a/packages/safe-chain/src/agent/runCommand.js
+++ b/packages/safe-chain/src/agent/runCommand.js
@@ -4,8 +4,6 @@ import chalk from "chalk";
import { initializeCliArguments } from "../config/cliArguments.js";
import { writeProxyState, clearProxyState } from "./proxyState.js";
import { getCaCertPath } from "../registryProxy/certUtils.js";
-import { setup } from "../shell-integration/setup.js";
-import { teardown } from "../shell-integration/teardown.js";
/**
* Run the Safe Chain proxy as a standalone service
@@ -26,9 +24,9 @@ export async function runCommand(args) {
// Initialize logging from args
initializeCliArguments(processedArgs);
- // Automatically set up shell integration
- await setup();
- ui.emptyLine();
+ // Note: We no longer call setup() here because the installer sets up
+ // system-wide proxy environment variables via LaunchAgent on macOS
+ // or systemd on Linux. The certificate is also installed at install time.
const service = new StandaloneProxyService({
autoVerify: false
@@ -54,11 +52,14 @@ export async function runCommand(args) {
ui.writeInformation(` PID: ${chalk.cyan(process.pid)}`);
ui.emptyLine();
- ui.writeInformation(chalk.bold("How to Use:"));
- ui.writeInformation(chalk.dim(" Restart your terminal, then run package managers normally:"));
- ui.writeInformation(chalk.cyan(" npm install "));
- ui.writeInformation(chalk.cyan(" yarn add "));
- ui.writeInformation(chalk.cyan(" pip3 install "));
+ ui.writeInformation(chalk.bold("Environment Variables Set:"));
+ ui.writeInformation(` ${chalk.cyan("HTTPS_PROXY")}: http://localhost:${port}`);
+ ui.writeInformation(` ${chalk.cyan("GLOBAL_AGENT_HTTP_PROXY")}: http://localhost:${port}`);
+ ui.writeInformation(` ${chalk.cyan("NODE_EXTRA_CA_CERTS")}: ${getCaCertPath()}`);
+ ui.emptyLine();
+
+ ui.writeInformation(chalk.bold("Package managers will use the proxy automatically."));
+ ui.writeInformation(chalk.dim(" No shell wrappers or aliases needed."));
ui.emptyLine();
ui.writeInformation(
@@ -104,9 +105,8 @@ export async function runCommand(args) {
try {
await service.stop();
- // Remove shell integration
- ui.emptyLine();
- await teardown();
+ // Note: We no longer call teardown() here because the environment
+ // variables are managed by the system service (LaunchAgent/systemd)
process.exit(0);
} catch (/** @type {any} */ error) {
diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js
index a2fb7bb..0838870 100644
--- a/packages/safe-chain/src/registryProxy/certUtils.js
+++ b/packages/safe-chain/src/registryProxy/certUtils.js
@@ -116,3 +116,15 @@ function generateCa() {
certificate: cert,
};
}
+
+/**
+ * Generate CA certificate and return as PEM strings
+ * @returns {{cert: string, key: string}}
+ */
+export function generateCACertificate() {
+ const { privateKey, certificate } = generateCa();
+ return {
+ cert: forge.pki.certificateToPem(certificate),
+ key: forge.pki.privateKeyToPem(privateKey),
+ };
+}