From 97bbc77162e1d97be4bd5ce43b93cae1a52ad4be Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 19 Nov 2025 13:15:39 -0800 Subject: [PATCH] Fix some scripting issues --- installer/README.md | 11 +++++ installer/build.js | 35 +++++++++---- installer/generate-certs.js | 2 +- installer/package.json | 2 +- installer/scripts/darwin_build_installer.sh | 21 ++++++++ installer/scripts/darwin_postinstall.sh | 49 ++++++++++--------- installer/scripts/darwin_uninstall.sh | 38 ++++++++++---- installer/scripts/linux_build_installer.sh | 5 ++ installer/scripts/windows_build_installer.bat | 5 ++ packages/safe-chain/bin/safe-chain.js | 12 ++--- .../safe-chain/src/registryProxy/certUtils.js | 2 +- 11 files changed, 129 insertions(+), 53 deletions(-) create mode 100644 installer/README.md create mode 100755 installer/scripts/darwin_build_installer.sh create mode 100755 installer/scripts/linux_build_installer.sh create mode 100644 installer/scripts/windows_build_installer.bat diff --git a/installer/README.md b/installer/README.md new file mode 100644 index 0000000..b293c2b --- /dev/null +++ b/installer/README.md @@ -0,0 +1,11 @@ +# Safe Chain Installer - WIP + +This directory contains the build scripts and resources for creating standalone Safe Chain installers for different platforms. + +## Overview + +The installer bundles the Safe Chain Node.js application into a standalone binary using [@yao-pkg/pkg](https://github.com/yao-pkg/pkg) and creates platform-specific installers that: + +1. Install the `safe-chain` binary to the system PATH +2. Generate and install the CA certificate in the OS trust store +3. Configure the system for automatic MITM proxy interception diff --git a/installer/build.js b/installer/build.js index 86b02ea..e139534 100644 --- a/installer/build.js +++ b/installer/build.js @@ -65,6 +65,11 @@ async function bundleWithEsbuild() { loader: { '.json': 'json', // Inline JSON files }, + // We need to inject a polyfill because: + // source code is ESM (uses import.meta.url) + // target is CommonJS (required by pkg) + // CommonJS doesn't have import.meta.url, and ESM doesn't have __filename/__dirname + // create a fake import.meta.url from __filename banner: { js: `// Polyfill for import.meta.url in CommonJS var __filename = __filename || (() => { @@ -85,13 +90,10 @@ var import_meta_url = typeof __filename !== 'undefined' ? require('url').pathToF sourcemap: false, }); - console.log('✓ Bundle created at:', outputFile); + console.log('Bundle created at:', outputFile); return outputFile; } -/** - * Build macOS binary and installer - */ async function buildMacOS() { console.log('Building macOS binary...'); @@ -195,12 +197,27 @@ async function createMacOSInstaller(binaryPath) { writeFileSync(join(scriptsDir, 'preinstall'), preinstallScript, { mode: 0o755 }); writeFileSync(join(scriptsDir, 'postinstall'), postinstallScript, { mode: 0o755 }); writeFileSync(join(installerDir, 'uninstall.sh'), uninstallScript, { mode: 0o755 }); + + // Run pkgbuild to create the .pkg file + console.log('Running pkgbuild...'); + const pkgName = 'SafeChain.pkg'; + const pkgPath = join(installerDir, pkgName); - 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'); + // Get version from package.json + const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')); + const version = packageJson.version; + + const pkgbuildArgs = [ + '--root', resourcesDir, + '--scripts', scriptsDir, + '--identifier', 'com.aikido.safe-chain', + '--version', version, + '--install-location', '/tmp/safe-chain-install', + pkgPath + ]; + + await execAsync(`pkgbuild ${pkgbuildArgs.join(' ')}`); + console.log(`Mac OS Installer created at: ${pkgPath}`); } /** diff --git a/installer/generate-certs.js b/installer/generate-certs.js index 440a542..891ae96 100644 --- a/installer/generate-certs.js +++ b/installer/generate-certs.js @@ -11,7 +11,7 @@ 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 + * 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}>} diff --git a/installer/package.json b/installer/package.json index bcc6cfb..2499edd 100644 --- a/installer/package.json +++ b/installer/package.json @@ -1,7 +1,7 @@ { "name": "@aikidosec/safe-chain-installer", "version": "1.0.0", - "description": "Installer for Aikido Safe Chain - creates standalone binaries and installs system certificates", + "description": "Installer for Aikido Safe Chain - creates standalone binaries and contains pre- and post install functionality", "private": true, "type": "module", "scripts": { diff --git a/installer/scripts/darwin_build_installer.sh b/installer/scripts/darwin_build_installer.sh new file mode 100755 index 0000000..91ade1c --- /dev/null +++ b/installer/scripts/darwin_build_installer.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -e + +# Get the directory where this script is located +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +INSTALLER_ROOT="${SCRIPT_DIR}/.." + +echo "=== Building Safe Chain Installer for macOS ===" + +# Ensure we are in the installer directory +cd "${INSTALLER_ROOT}" + +# Install dependencies +echo "Installing build dependencies..." +npm install + +# Build the binary and installer using the Node.js build script +echo "Building binary and installer..." +node build.js --platform=macos + +echo "Done." diff --git a/installer/scripts/darwin_postinstall.sh b/installer/scripts/darwin_postinstall.sh index 54ae030..3db74bf 100644 --- a/installer/scripts/darwin_postinstall.sh +++ b/installer/scripts/darwin_postinstall.sh @@ -12,32 +12,32 @@ 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" +# Setup system-wide certificate directory +# We use a shared location so we don't need to worry about which user is running the agent +CERT_DIR="/usr/local/share/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}" + # Run as root (installer context) - no need to switch users + /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" +# Set permissions so any user can read the certs (required for the agent to load them) +# Directory is executable/readable by all +chmod 755 "/usr/local/share/safe-chain" +chmod 755 "${CERT_DIR}" + +# PUBLIC Certificate: Readable by everyone (644) +chmod 644 "${CERT_DIR}/ca-cert.pem" + +# PRIVATE Key: Readable ONLY by the owner (600) +# This is critical for security. +chmod 600 "${CERT_DIR}/ca-key.pem" + +# Ensure the actual user owns the files so the agent (running as user) can read them +chown -R "${ACTUAL_USER}:staff" "/usr/local/share/safe-chain" # Install certificate in system trust store echo "Installing Safe Chain CA certificate in system trust store..." @@ -52,9 +52,15 @@ fi # Start safe-chain as a background service echo "Starting Safe Chain proxy service..." +# Get the actual user to install the LaunchAgent in their home +# (We still need this for the LaunchAgent, but not for file permissions) +ACTUAL_USER=$(stat -f '%Su' /dev/console) +USER_HOME=$(eval echo "~${ACTUAL_USER}") + # Create LaunchAgent for auto-start on login LAUNCH_AGENT_DIR="${USER_HOME}/Library/LaunchAgents" mkdir -p "${LAUNCH_AGENT_DIR}" +chown "${ACTUAL_USER}:staff" "${LAUNCH_AGENT_DIR}" PLIST_PATH="${LAUNCH_AGENT_DIR}/com.aikido.safe-chain.plist" cat > "${PLIST_PATH}" << EOF @@ -77,6 +83,8 @@ cat > "${PLIST_PATH}" << EOF http://localhost:8080 NODE_EXTRA_CA_CERTS ${CERT_DIR}/ca-cert.pem + SAFE_CHAIN_CERT_DIR + ${CERT_DIR} RunAtLoad @@ -93,9 +101,6 @@ 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 diff --git a/installer/scripts/darwin_uninstall.sh b/installer/scripts/darwin_uninstall.sh index 79ab745..484d449 100644 --- a/installer/scripts/darwin_uninstall.sh +++ b/installer/scripts/darwin_uninstall.sh @@ -3,36 +3,54 @@ set -e echo "Uninstalling Safe Chain..." -USER_HOME="${HOME}" -if [ -z "${USER_HOME}" ]; then - USER_HOME=~ +# Get the actual user +if [ -n "${SUDO_USER}" ]; then + ACTUAL_USER="${SUDO_USER}" +else + ACTUAL_USER=$(stat -f '%Su' /dev/console) fi +# Get the home directory of the actual user +USER_HOME=$(eval echo "~${ACTUAL_USER}") + +echo "Detected user: ${ACTUAL_USER}" +echo "User home: ${USER_HOME}" + # Stop and unload the LaunchAgent PLIST_PATH="${USER_HOME}/Library/LaunchAgents/com.aikido.safe-chain.plist" +SERVICE_LABEL="com.aikido.safe-chain" + +echo "Stopping Safe Chain service..." if [ -f "${PLIST_PATH}" ]; then - echo "Stopping Safe Chain service..." - launchctl unload "${PLIST_PATH}" 2>/dev/null || true + # Run launchctl as the user + sudo -u "${ACTUAL_USER}" launchctl unload "${PLIST_PATH}" 2>/dev/null || true rm -f "${PLIST_PATH}" fi +# Ensure service is removed even if plist is gone +sudo -u "${ACTUAL_USER}" launchctl remove "${SERVICE_LABEL}" 2>/dev/null || true + # 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 +# Run launchctl as the user +sudo -u "${ACTUAL_USER}" launchctl unsetenv HTTPS_PROXY 2>/dev/null || true +sudo -u "${ACTUAL_USER}" launchctl unsetenv GLOBAL_AGENT_HTTP_PROXY 2>/dev/null || true +sudo -u "${ACTUAL_USER}" 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 +CERT_DIR="/usr/local/share/safe-chain/certs" +if [ -f "${CERT_DIR}/ca-cert.pem" ]; 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 +# Remove system-wide configuration and certificates +rm -rf /usr/local/share/safe-chain + # Optionally remove the .safe-chain directory (commented out to preserve user data) # echo "Remove ~/.safe-chain directory? (y/N)" # read -r response diff --git a/installer/scripts/linux_build_installer.sh b/installer/scripts/linux_build_installer.sh new file mode 100755 index 0000000..8d8d3ea --- /dev/null +++ b/installer/scripts/linux_build_installer.sh @@ -0,0 +1,5 @@ +#!/bin/bash +echo "=== Building Safe Chain Installer for Linux ===" +echo "TODO: Implement Linux installer build" +echo "This is a placeholder script." +exit 0 diff --git a/installer/scripts/windows_build_installer.bat b/installer/scripts/windows_build_installer.bat new file mode 100644 index 0000000..709ce23 --- /dev/null +++ b/installer/scripts/windows_build_installer.bat @@ -0,0 +1,5 @@ +@echo off +echo === Building Safe Chain Installer for Windows === +echo TODO: Implement Windows installer build +echo This is a placeholder script. +exit /b 0 diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 011e460..4afd2e7 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -36,7 +36,8 @@ if (command === "setup") { // Pass remaining arguments to runCommand const runArgs = process.argv.slice(3); runCommand(runArgs); -} else if (command === "generate-cert") { +} else if (command === "_generate-cert") { + // Internal command for installer // Pass remaining arguments to generateCertCommand const certArgs = process.argv.slice(3); generateCertCommand(certArgs); @@ -59,9 +60,7 @@ function writeHelp() { ui.writeInformation( `Available commands: ${chalk.cyan("setup")}, ${chalk.cyan( "teardown" - )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("run")}, ${chalk.cyan( - "generate-cert" - )}, ${chalk.cyan("help")}, ${chalk.cyan("--version")}` + )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("run")}, ${chalk.cyan("help")}, ${chalk.cyan("--version")}` ); ui.emptyLine(); ui.writeInformation( @@ -84,11 +83,6 @@ function writeHelp() { "safe-chain run" )}: 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( "safe-chain --version" diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index 0838870..add4b68 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -3,7 +3,7 @@ import path from "path"; import fs from "fs"; import os from "os"; -const certFolder = path.join(os.homedir(), ".safe-chain", "certs"); +const certFolder = process.env.SAFE_CHAIN_CERT_DIR || path.join(os.homedir(), ".safe-chain", "certs"); const ca = loadCa(); const certCache = new Map();