@chinchillaenterprises/mcp-amplify
Version:
AWS Amplify MCP server with intelligent deployment automation, specialized logging suite, and recursive resource discovery
162 lines • 7.32 kB
JavaScript
import * as pty from 'node-pty';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
/**
* Set an Amplify sandbox secret programmatically without value corruption
*
* This function uses node-pty (pseudo-terminal) to interact with the Amplify CLI
* as if it were a real terminal, allowing it to handle the interactive prompt properly.
* This avoids invisible character corruption that occurs with bash piping or stdin pipes.
*/
export async function handleAmplifySetSandboxSecret(args) {
const { secretName, secretValue, sandboxIdentifier } = args;
// Validation
if (!secretName || typeof secretName !== 'string') {
throw new Error('secretName is required and must be a string');
}
if (secretValue === undefined || secretValue === null) {
throw new Error('secretValue is required');
}
// Convert secretValue to string (in case it's a number or boolean)
const secretValueStr = String(secretValue);
if (secretValueStr.length === 0) {
throw new Error('secretValue cannot be empty');
}
// Warn if secret is very long (SSM limit is 8KB for SecureString)
const sizeInBytes = Buffer.byteLength(secretValueStr, 'utf8');
if (sizeInBytes > 8192) {
throw new Error(`Secret value is too large (${sizeInBytes} bytes). AWS SSM limit is 8192 bytes for SecureString.`);
}
// Validate secret name format (alphanumeric, underscores, hyphens)
if (!/^[A-Z0-9_-]+$/i.test(secretName)) {
throw new Error('Secret name can only contain letters, numbers, underscores, and hyphens');
}
try {
// Check if we're in an Amplify project directory
const isAmplifyProject = await checkAmplifyProject();
if (!isAmplifyProject) {
throw new Error('Not in an Amplify project directory. Please run this command from your Amplify project root.');
}
// Build command arguments
const args_array = ['ampx', 'sandbox', 'secret', 'set', secretName];
if (sandboxIdentifier) {
args_array.push('--identifier', sandboxIdentifier);
}
// Execute the command with controlled stdin
const result = await executeAmplifySecretCommand(args_array, secretValueStr);
return {
success: true,
secretName,
sandboxIdentifier: sandboxIdentifier || '(default sandbox)',
message: `Secret '${secretName}' set successfully`,
details: {
secretLength: secretValueStr.length,
sizeInBytes,
hasSpecialChars: /[^a-zA-Z0-9]/.test(secretValueStr),
isMultiline: secretValueStr.includes('\n')
},
verification: {
command: sandboxIdentifier
? `npx ampx sandbox secret get ${secretName} --identifier ${sandboxIdentifier}`
: `npx ampx sandbox secret get ${secretName}`,
description: 'Run this command to verify the secret was set correctly'
},
nextSteps: [
'Secret is now available in your Lambda functions',
'Access it via process.env.' + secretName,
'Restart your sandbox if it was already running: npx ampx sandbox'
]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// Provide helpful error messages based on common issues
if (errorMessage.includes('not found') || errorMessage.includes('command not found')) {
throw new Error('Amplify CLI not found. Please install it: npm install -g @aws-amplify/cli');
}
if (errorMessage.includes('credentials')) {
throw new Error('AWS credentials not configured. Run: aws configure');
}
if (errorMessage.includes('permission') || errorMessage.includes('Access Denied')) {
throw new Error('Permission denied. Ensure your AWS IAM user has SSM permissions (ssm:PutParameter, ssm:GetParameter)');
}
throw new Error(`Failed to set sandbox secret: ${errorMessage}`);
}
}
/**
* Execute the Amplify secret set command using PTY (pseudo-terminal)
*
* PTY creates a fake terminal that the CLI accepts as real, allowing us to
* interact with interactive prompts that would otherwise hang when using stdin pipes.
*/
function executeAmplifySecretCommand(args, secretValue) {
return new Promise((resolve, reject) => {
// Determine the appropriate shell based on platform
const shell = os.platform() === 'win32' ? 'powershell.exe' : process.env.SHELL || '/bin/bash';
// Create a pseudo-terminal with npx command
const ptyProcess = pty.spawn('npx', args, {
name: 'xterm-color',
cols: 80,
rows: 30,
cwd: process.cwd(),
env: process.env
});
let output = '';
let hasWrittenSecret = false;
// Listen to all data from the PTY
ptyProcess.onData((data) => {
output += data;
// Detect the interactive prompt for secret value
// The prompt looks like: "? Enter secret value: " or similar
if (!hasWrittenSecret && (data.includes('Enter secret value') ||
data.includes('secret value:') ||
data.includes('Enter value'))) {
// Write the secret value using \r (carriage return) for terminal
ptyProcess.write(secretValue + '\r');
hasWrittenSecret = true;
}
});
// Handle process exit
ptyProcess.onExit(({ exitCode, signal }) => {
// Clean up ANSI escape codes from output for cleaner response
const cleanOutput = output.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
// Check for success indicators
if (exitCode === 0 && (cleanOutput.includes('successfully') ||
cleanOutput.includes('Secret set') ||
cleanOutput.includes('Stored secret') ||
hasWrittenSecret)) {
resolve(cleanOutput);
}
else if (exitCode === 0 && !hasWrittenSecret) {
// Command succeeded but we never saw the prompt - might be an issue
reject(new Error('Command completed but secret prompt was not detected. Output: ' + cleanOutput));
}
else if (exitCode !== 0) {
reject(new Error(`Command failed with exit code ${exitCode}${signal ? ` (signal: ${signal})` : ''}: ${cleanOutput}`));
}
else {
resolve(cleanOutput);
}
});
// Timeout after 60 seconds (increased from 30 for slower systems)
setTimeout(() => {
ptyProcess.kill();
reject(new Error('Command timed out after 60 seconds'));
}, 60000);
});
}
/**
* Check if current directory is an Amplify project
*/
async function checkAmplifyProject() {
try {
const amplifyDir = path.join(process.cwd(), 'amplify');
const stat = await fs.stat(amplifyDir);
return stat.isDirectory();
}
catch (error) {
return false;
}
}
//# sourceMappingURL=sandbox-secrets-handlers.js.map