Add installer build scripts and configuration

This commit is contained in:
Reinier Criel 2025-11-25 08:21:35 -08:00
parent fb3a8582a2
commit 3420290ea9
22 changed files with 1377 additions and 7 deletions

View file

@ -0,0 +1,334 @@
#!/usr/bin/env node
/**
* Main build script for creating the macOS .pkg installer
*
* 1. Clean previous builds
* 2. Bundle Node.js runtime
* 3. Bundle agent code and dependencies
* 4. Generate CA certificate
* 5. Create installer scripts
* 6. Build .pkg with pkgbuild and productbuild
* 7. Sign the package (if certificates available)
*/
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.join(__dirname, '..');
const buildDir = path.join(rootDir, 'build');
const distDir = path.join(rootDir, 'dist');
console.log('🏗️ Building Aikido Safe Chain macOS Installer...\n');
// Step 1: Clean and create directories
console.log('Preparing build directories...');
if (fs.existsSync(buildDir)) {
fs.rmSync(buildDir, { recursive: true });
}
if (fs.existsSync(distDir)) {
fs.rmSync(distDir, { recursive: true });
}
fs.mkdirSync(buildDir, { recursive: true });
fs.mkdirSync(distDir, { recursive: true });
const payloadDir = path.join(distDir, 'payload');
const installRoot = path.join(payloadDir, 'Library/Application Support/AikidoSafety');
fs.mkdirSync(installRoot, { recursive: true });
console.log('Directories created\n');
// Step 2: Bundle Node.js runtime
console.log('Bundling Node.js runtime...');
await bundleNodeRuntime();
console.log('Node.js bundled\n');
// Step 3: Bundle agent code
console.log('Bundling agent code...');
await bundleAgentCode();
console.log('Agent code bundled\n');
// Step 4: Generate certificates
console.log('Generating CA certificate...');
await generateCertificates();
console.log('Certificates generated\n');
// Step 5: Create LaunchDaemon plist
console.log('Creating LaunchDaemon configuration...');
await createLaunchDaemonPlist();
console.log('LaunchDaemon plist created\n');
// Step 6: Create installer scripts
console.log('Creating installer scripts...');
await createInstallerScripts();
console.log('Installer scripts created\n');
// Step 7: Create uninstaller
console.log('Creating uninstaller...');
await createUninstaller();
console.log('Uninstaller created\n');
// Step 8: Build package
console.log('Building .pkg installer...');
await buildPackage();
console.log('Package built\n');
console.log('Build complete!');
console.log(`\nInstaller: ${path.join(buildDir, 'AikidoSafeChain.pkg')}`);
console.log(`Uninstaller: ${path.join(buildDir, 'uninstall.sh')}\n`);
/**
* Bundle Node.js runtime from current installation
*/
async function bundleNodeRuntime() {
const binDir = path.join(installRoot, 'bin');
fs.mkdirSync(binDir, { recursive: true });
// Copy current Node.js binary
const nodePath = process.execPath;
const targetNodePath = path.join(binDir, 'node');
fs.copyFileSync(nodePath, targetNodePath);
fs.chmodSync(targetNodePath, 0o755);
console.log(` Copied Node.js ${process.version} to ${targetNodePath}`);
}
/**
* Bundle agent code and dependencies
*/
async function bundleAgentCode() {
const agentDir = path.join(installRoot, 'agent');
fs.mkdirSync(agentDir, { recursive: true });
// Copy agent source files
const agentSrc = path.join(rootDir, 'agent');
copyDirectory(agentSrc, agentDir);
// Copy necessary dependencies from packages/safe-chain
const safeChainSrc = path.join(rootDir, '../packages/safe-chain/src');
const safeChainDest = path.join(agentDir, 'lib');
fs.mkdirSync(safeChainDest, { recursive: true });
// Copy only registry proxy code (reused by agent)
copyDirectory(
path.join(safeChainSrc, 'registryProxy'),
path.join(safeChainDest, 'registryProxy')
);
copyDirectory(
path.join(safeChainSrc, 'scanning'),
path.join(safeChainDest, 'scanning')
);
copyDirectory(
path.join(safeChainSrc, 'api'),
path.join(safeChainDest, 'api')
);
copyDirectory(
path.join(safeChainSrc, 'config'),
path.join(safeChainDest, 'config')
);
copyDirectory(
path.join(safeChainSrc, 'environment'),
path.join(safeChainDest, 'environment')
);
copyDirectory(
path.join(safeChainSrc, 'utils'),
path.join(safeChainDest, 'utils')
);
// Install production dependencies
const agentPackageJson = path.join(agentDir, 'package.json');
if (fs.existsSync(agentPackageJson)) {
console.log(' Installing agent dependencies...');
execSync('npm install --production --no-optional', {
cwd: agentDir,
stdio: 'inherit'
});
}
console.log(` Agent code bundled to ${agentDir}`);
}
/**
* Generate CA certificate for MITM proxy
* Reuses certificate generation code from safe-chain
*/
async function generateCertificates() {
const certsDir = path.join(installRoot, 'certs');
fs.mkdirSync(certsDir, { recursive: true });
// Import certificate generation from safe-chain
const certUtilsPath = path.join(rootDir, '../packages/safe-chain/src/registryProxy/certUtils.js');
const { generateCa } = await import(certUtilsPath);
const { default: forge } = await import('node-forge');
// Generate CA certificate with system-wide agent attributes
// (10 year validity vs 1 day for CLI, full org details for system keychain)
const { privateKey, certificate } = generateCa({
attrs: [
{ name: 'commonName', value: 'Aikido Safe Chain CA' },
{ name: 'countryName', value: 'US' },
{ shortName: 'ST', value: 'California' },
{ name: 'localityName', value: 'San Francisco' },
{ name: 'organizationName', value: 'Aikido Security' },
{ shortName: 'OU', value: 'Safe Chain' }
],
validityDays: 3650 // 10 years
});
// Write certificate and key
const certPem = forge.pki.certificateToPem(certificate);
const keyPem = forge.pki.privateKeyToPem(privateKey);
fs.writeFileSync(path.join(certsDir, 'ca-cert.pem'), certPem);
fs.writeFileSync(path.join(certsDir, 'ca-key.pem'), keyPem);
fs.chmodSync(path.join(certsDir, 'ca-key.pem'), 0o600);
console.log(` CA certificate generated in ${certsDir}`);
}
/**
* Create LaunchDaemon plist file
*/
async function createLaunchDaemonPlist() {
const plistDir = path.join(distDir, 'payload/Library/LaunchDaemons');
fs.mkdirSync(plistDir, { recursive: true });
const templatesDir = path.join(__dirname, 'templates');
// Read plist template
const plist = fs.readFileSync(path.join(templatesDir, 'dev.aikido.safe-chain.plist'), 'utf-8');
fs.writeFileSync(path.join(plistDir, 'dev.aikido.safe-chain.plist'), plist);
console.log(` LaunchDaemon plist created`);
}
/**
* Create installer pre/post install scripts
*/
async function createInstallerScripts() {
const scriptsDir = path.join(distDir, 'scripts');
fs.mkdirSync(scriptsDir, { recursive: true });
const templatesDir = path.join(__dirname, 'templates');
// Read script templates
const postinstall = fs.readFileSync(path.join(templatesDir, 'postinstall.sh'), 'utf-8');
const preinstall = fs.readFileSync(path.join(templatesDir, 'preinstall.sh'), 'utf-8');
// Write scripts to dist directory
fs.writeFileSync(path.join(scriptsDir, 'postinstall'), postinstall);
fs.writeFileSync(path.join(scriptsDir, 'preinstall'), preinstall);
fs.chmodSync(path.join(scriptsDir, 'postinstall'), 0o755);
fs.chmodSync(path.join(scriptsDir, 'preinstall'), 0o755);
console.log(` Installer scripts created in ${scriptsDir}`);
}
/**
* Create uninstaller script
*/
async function createUninstaller() {
const templatesDir = path.join(__dirname, 'templates');
// Read uninstaller template
const uninstallScript = fs.readFileSync(path.join(templatesDir, 'uninstall.sh'), 'utf-8');
// Write to both build and payload
fs.writeFileSync(path.join(buildDir, 'uninstall.sh'), uninstallScript);
fs.chmodSync(path.join(buildDir, 'uninstall.sh'), 0o755);
const installUninstallPath = path.join(installRoot, 'uninstall.sh');
fs.writeFileSync(installUninstallPath, uninstallScript);
fs.chmodSync(installUninstallPath, 0o755);
console.log(` Uninstaller created in ${buildDir}`);
}
/**
* Build the .pkg installer
*/
async function buildPackage() {
const componentPkg = path.join(buildDir, 'component.pkg');
const finalPkg = path.join(buildDir, 'AikidoSafeChain.pkg');
// Build component package
const pkgbuildCmd = [
'pkgbuild',
'--root', `"${path.join(distDir, 'payload')}"`,
'--scripts', `"${path.join(distDir, 'scripts')}"`,
'--identifier', 'dev.aikido.safe-chain',
'--version', '1.0.0',
'--install-location', '/',
`"${componentPkg}"`
].join(' ');
console.log(` Running: ${pkgbuildCmd}`);
execSync(pkgbuildCmd, { stdio: 'inherit' });
const templatesDir = path.join(__dirname, 'templates');
// Read distribution XML template
const distribution = fs.readFileSync(path.join(templatesDir, 'distribution.xml'), 'utf-8');
const distributionPath = path.join(buildDir, 'distribution.xml');
fs.writeFileSync(distributionPath, distribution);
// Create resources
const resourcesDir = path.join(buildDir, 'resources');
fs.mkdirSync(resourcesDir, { recursive: true });
// Read HTML templates
const welcomeHtml = fs.readFileSync(path.join(templatesDir, 'welcome.html'), 'utf-8');
const conclusionHtml = fs.readFileSync(path.join(templatesDir, 'conclusion.html'), 'utf-8');
// Write HTML files to resources directory
fs.writeFileSync(path.join(resourcesDir, 'welcome.html'), welcomeHtml);
fs.writeFileSync(path.join(resourcesDir, 'conclusion.html'), conclusionHtml);
// Build final package
const productbuildCmd = [
'productbuild',
'--distribution', `"${distributionPath}"`,
'--resources', `"${resourcesDir}"`,
'--package-path', `"${buildDir}"`,
`"${finalPkg}"`
].join(' ');
console.log(` Running: ${productbuildCmd}`);
execSync(productbuildCmd, { stdio: 'inherit' });
console.log(` Package created: ${finalPkg}`);
}
/**
* Helper: Copy directory recursively
*/
function copyDirectory(src, dest) {
if (!fs.existsSync(src)) {
console.warn(` Warning: Source directory not found: ${src}`);
return;
}
fs.mkdirSync(dest, { recursive: true });
const entries = fs.readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDirectory(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
<h1>Installation Complete!</h1>
<p>blablabla.</p>
<p><strong>To uninstall:</strong></p>
<pre>sudo bash "/Library/Application Support/AikidoSafety/uninstall.sh"</pre>
<p>For support, visit: <a href="https://aikido.dev">aikido.dev</a></p>
</body>
</html>

View file

@ -0,0 +1,31 @@
<?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>dev.aikido.safe-chain</string>
<key>ProgramArguments</key>
<array>
<string>/Library/Application Support/AikidoSafety/bin/node</string>
<string>/Library/Application Support/AikidoSafety/agent/index.js</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>StandardOutPath</key>
<string>/var/log/aikido-safe-chain/stdout.log</string>
<key>StandardErrorPath</key>
<string>/var/log/aikido-safe-chain/stderr.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>NODE_ENV</key>
<string>production</string>
</dict>
<key>WorkingDirectory</key>
<string>/Library/Application Support/AikidoSafety/agent</string>
</dict>
</plist>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="1">
<title>Aikido Safe Chain</title>
<organization>dev.aikido</organization>
<domains enable_localSystem="true"/>
<options customize="never" require-scripts="true" rootVolumeOnly="true" />
<welcome file="welcome.html"/>
<conclusion file="conclusion.html"/>
<pkg-ref id="dev.aikido.safe-chain"/>
<options customize="never" require-scripts="true"/>
<choices-outline>
<line choice="default">
<line choice="dev.aikido.safe-chain"/>
</line>
</choices-outline>
<choice id="default"/>
<choice id="dev.aikido.safe-chain" visible="false">
<pkg-ref id="dev.aikido.safe-chain"/>
</choice>
<pkg-ref id="dev.aikido.safe-chain" version="1.0.0" onConclusion="none">component.pkg</pkg-ref>
</installer-gui-script>

View file

@ -0,0 +1,40 @@
#!/bin/bash
set -e
INSTALL_DIR="/Library/Application Support/AikidoSafety"
LAUNCHD_PLIST="/Library/LaunchDaemons/dev.aikido.safe-chain.plist"
LOG_DIR="/var/log/aikido-safe-chain"
echo "Installing Aikido Safe Chain Agent..."
# Create log directory
mkdir -p "$LOG_DIR"
chmod 755 "$LOG_DIR"
# Install certificate to system keychain
echo "Installing CA certificate to system keychain..."
security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain \
"$INSTALL_DIR/certs/ca-cert.pem" || true
# Configure system proxy
echo "Configuring system proxy settings..."
"$INSTALL_DIR/bin/node" "$INSTALL_DIR/agent/configure-proxy.js" --install || {
echo "Warning: Failed to configure system proxy. You may need to configure manually."
}
# Load and start the LaunchDaemon
echo "Starting Aikido Safe Chain Agent..."
launchctl load -w "$LAUNCHD_PLIST" || {
echo "Warning: Failed to start agent. You may need to restart your computer."
}
echo "Aikido Safe Chain Agent installed successfully!"
echo ""
echo "The agent is now running in the background and will protect"
echo "all package installations on this system."
echo ""
echo "To uninstall, run:"
echo " sudo bash '$INSTALL_DIR/uninstall.sh'"
exit 0

View file

@ -0,0 +1,12 @@
#!/bin/bash
set -e
LAUNCHD_PLIST="/Library/LaunchDaemons/dev.aikido.safe-chain.plist"
# Stop existing agent if running
if [ -f "$LAUNCHD_PLIST" ]; then
echo "Stopping existing Aikido Safe Chain Agent..."
launchctl unload "$LAUNCHD_PLIST" 2>/dev/null || true
fi
exit 0

View file

@ -0,0 +1,39 @@
#!/bin/bash
# Aikido Safe Chain Uninstaller
set -e
echo "Uninstalling Aikido Safe Chain Agent..."
INSTALL_DIR="/Library/Application Support/AikidoSafety"
LAUNCHD_PLIST="/Library/LaunchDaemons/dev.aikido.safe-chain.plist"
# Stop and remove daemon
if [ -f "$LAUNCHD_PLIST" ]; then
echo "Stopping agent..."
launchctl unload "$LAUNCHD_PLIST" 2>/dev/null || true
rm "$LAUNCHD_PLIST"
fi
# Remove certificate
echo "Removing CA certificate..."
security delete-certificate -c "Aikido Safe Chain CA" \
/Library/Keychains/System.keychain 2>/dev/null || true
# Restore proxy settings
if [ -f "$INSTALL_DIR/agent/configure-proxy.js" ]; then
echo "Restoring proxy settings..."
"$INSTALL_DIR/bin/node" "$INSTALL_DIR/agent/configure-proxy.js" --uninstall || {
echo "Warning: Failed to restore proxy settings. You may need to restore manually."
}
fi
# Remove files
echo "Removing files..."
rm -rf "$INSTALL_DIR"
rm -rf /var/log/aikido-safe-chain
echo ""
echo "✅ Aikido Safe Chain has been uninstalled."
echo ""
echo "Your system proxy settings have been restored to their original state."

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
<h1>Welcome to Aikido Safe Chain</h1>
<p>blablabla</p>
<p><strong>Note:</strong> This installer requires administrator privileges to:</p>
<ul>
<li>Install a trusted certificate in your system keychain</li>
<li>Configure system-wide proxy settings</li>
<li>Install a background service (LaunchDaemon)</li>
</ul>
</body>
</html>