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), + }; +}