mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Add installer changes
This commit is contained in:
parent
e765ccf303
commit
2158478894
13 changed files with 674 additions and 21 deletions
18
installer/.gitignore
vendored
Normal file
18
installer/.gitignore
vendored
Normal file
|
|
@ -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*
|
||||||
254
installer/build.js
Normal file
254
installer/build.js
Normal file
|
|
@ -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();
|
||||||
46
installer/generate-certs.js
Normal file
46
installer/generate-certs.js
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
22
installer/package.json
Normal file
22
installer/package.json
Normal file
|
|
@ -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": {}
|
||||||
|
}
|
||||||
128
installer/scripts/darwin_postinstall.sh
Normal file
128
installer/scripts/darwin_postinstall.sh
Normal file
|
|
@ -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
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.aikido.safe-chain</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/usr/local/bin/safe-chain</string>
|
||||||
|
<string>run</string>
|
||||||
|
</array>
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>HTTPS_PROXY</key>
|
||||||
|
<string>http://localhost:8080</string>
|
||||||
|
<key>GLOBAL_AGENT_HTTP_PROXY</key>
|
||||||
|
<string>http://localhost:8080</string>
|
||||||
|
<key>NODE_EXTRA_CA_CERTS</key>
|
||||||
|
<string>${CERT_DIR}/ca-cert.pem</string>
|
||||||
|
</dict>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>${USER_HOME}/.safe-chain/safe-chain.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>${USER_HOME}/.safe-chain/safe-chain.error.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
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
|
||||||
28
installer/scripts/darwin_preinstall.sh
Normal file
28
installer/scripts/darwin_preinstall.sh
Normal file
|
|
@ -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
|
||||||
49
installer/scripts/darwin_uninstall.sh
Normal file
49
installer/scripts/darwin_uninstall.sh
Normal file
|
|
@ -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
|
||||||
|
|
@ -4,13 +4,15 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"test/e2e"
|
"test/e2e",
|
||||||
|
"installer"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun",
|
"test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun",
|
||||||
"test:e2e": "npm run test --workspace=test/e2e",
|
"test:e2e": "npm run test --workspace=test/e2e",
|
||||||
"lint": "npm run lint --workspace=packages/safe-chain",
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,15 @@
|
||||||
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { createRequire } from "module";
|
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 { ui } from "../src/environment/userInteraction.js";
|
||||||
import { setup } from "../src/shell-integration/setup.js";
|
import { setup } from "../src/shell-integration/setup.js";
|
||||||
import { teardown } from "../src/shell-integration/teardown.js";
|
import { teardown } from "../src/shell-integration/teardown.js";
|
||||||
import { setupCi } from "../src/shell-integration/setup-ci.js";
|
import { setupCi } from "../src/shell-integration/setup-ci.js";
|
||||||
import { runCommand } from "../src/agent/runCommand.js";
|
import { runCommand } from "../src/agent/runCommand.js";
|
||||||
|
import { generateCertCommand } from "../src/agent/generateCert.js";
|
||||||
|
|
||||||
if (process.argv.length < 3) {
|
if (process.argv.length < 3) {
|
||||||
ui.writeError("No command provided. Please provide a command to execute.");
|
ui.writeError("No command provided. Please provide a command to execute.");
|
||||||
|
|
@ -32,6 +36,10 @@ if (command === "setup") {
|
||||||
// Pass remaining arguments to runCommand
|
// Pass remaining arguments to runCommand
|
||||||
const runArgs = process.argv.slice(3);
|
const runArgs = process.argv.slice(3);
|
||||||
runCommand(runArgs);
|
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") {
|
} else if (command === "--version" || command === "-v" || command === "-v") {
|
||||||
ui.writeInformation(`Current safe-chain version: ${getVersion()}`);
|
ui.writeInformation(`Current safe-chain version: ${getVersion()}`);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -52,8 +60,8 @@ function writeHelp() {
|
||||||
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
|
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
|
||||||
"teardown"
|
"teardown"
|
||||||
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("run")}, ${chalk.cyan(
|
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("run")}, ${chalk.cyan(
|
||||||
"help"
|
"generate-cert"
|
||||||
)}, ${chalk.cyan("--version")}`
|
)}, ${chalk.cyan("help")}, ${chalk.cyan("--version")}`
|
||||||
);
|
);
|
||||||
ui.emptyLine();
|
ui.emptyLine();
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
|
|
@ -74,7 +82,12 @@ function writeHelp() {
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`- ${chalk.cyan(
|
`- ${chalk.cyan(
|
||||||
"safe-chain run"
|
"safe-chain run"
|
||||||
)}: Run the proxy as a standalone service. Options: --all (default), --js, --py, --ecosystem=<type>`
|
)}: 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 <directory>`
|
||||||
);
|
);
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`- ${chalk.cyan(
|
`- ${chalk.cyan(
|
||||||
|
|
@ -85,7 +98,15 @@ function writeHelp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVersion() {
|
function getVersion() {
|
||||||
const require = createRequire(import.meta.url);
|
try {
|
||||||
const packageJson = require("../package.json");
|
// Try to load package.json from the expected location
|
||||||
return packageJson.version;
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,5 +64,18 @@
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/AikidoSec/safe-chain.git",
|
"url": "git+https://github.com/AikidoSec/safe-chain.git",
|
||||||
"directory": "packages/safe-chain"
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
60
packages/safe-chain/src/agent/generateCert.js
Normal file
60
packages/safe-chain/src/agent/generateCert.js
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,6 @@ import chalk from "chalk";
|
||||||
import { initializeCliArguments } from "../config/cliArguments.js";
|
import { initializeCliArguments } from "../config/cliArguments.js";
|
||||||
import { writeProxyState, clearProxyState } from "./proxyState.js";
|
import { writeProxyState, clearProxyState } from "./proxyState.js";
|
||||||
import { getCaCertPath } from "../registryProxy/certUtils.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
|
* Run the Safe Chain proxy as a standalone service
|
||||||
|
|
@ -26,9 +24,9 @@ export async function runCommand(args) {
|
||||||
// Initialize logging from args
|
// Initialize logging from args
|
||||||
initializeCliArguments(processedArgs);
|
initializeCliArguments(processedArgs);
|
||||||
|
|
||||||
// Automatically set up shell integration
|
// Note: We no longer call setup() here because the installer sets up
|
||||||
await setup();
|
// system-wide proxy environment variables via LaunchAgent on macOS
|
||||||
ui.emptyLine();
|
// or systemd on Linux. The certificate is also installed at install time.
|
||||||
|
|
||||||
const service = new StandaloneProxyService({
|
const service = new StandaloneProxyService({
|
||||||
autoVerify: false
|
autoVerify: false
|
||||||
|
|
@ -54,11 +52,14 @@ export async function runCommand(args) {
|
||||||
ui.writeInformation(` PID: ${chalk.cyan(process.pid)}`);
|
ui.writeInformation(` PID: ${chalk.cyan(process.pid)}`);
|
||||||
ui.emptyLine();
|
ui.emptyLine();
|
||||||
|
|
||||||
ui.writeInformation(chalk.bold("How to Use:"));
|
ui.writeInformation(chalk.bold("Environment Variables Set:"));
|
||||||
ui.writeInformation(chalk.dim(" Restart your terminal, then run package managers normally:"));
|
ui.writeInformation(` ${chalk.cyan("HTTPS_PROXY")}: http://localhost:${port}`);
|
||||||
ui.writeInformation(chalk.cyan(" npm install <package>"));
|
ui.writeInformation(` ${chalk.cyan("GLOBAL_AGENT_HTTP_PROXY")}: http://localhost:${port}`);
|
||||||
ui.writeInformation(chalk.cyan(" yarn add <package>"));
|
ui.writeInformation(` ${chalk.cyan("NODE_EXTRA_CA_CERTS")}: ${getCaCertPath()}`);
|
||||||
ui.writeInformation(chalk.cyan(" pip3 install <package>"));
|
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.emptyLine();
|
||||||
|
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
|
|
@ -104,9 +105,8 @@ export async function runCommand(args) {
|
||||||
try {
|
try {
|
||||||
await service.stop();
|
await service.stop();
|
||||||
|
|
||||||
// Remove shell integration
|
// Note: We no longer call teardown() here because the environment
|
||||||
ui.emptyLine();
|
// variables are managed by the system service (LaunchAgent/systemd)
|
||||||
await teardown();
|
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} catch (/** @type {any} */ error) {
|
} catch (/** @type {any} */ error) {
|
||||||
|
|
|
||||||
|
|
@ -116,3 +116,15 @@ function generateCa() {
|
||||||
certificate: cert,
|
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue