This commit is contained in:
Siddharth Ganesan
2025-07-07 13:15:12 -07:00
parent 8c157083bc
commit 67030d9576
5 changed files with 527 additions and 29 deletions

View File

@@ -0,0 +1,344 @@
import {
GetFunctionCommand,
GetFunctionConfigurationCommand,
LambdaClient,
} from '@aws-sdk/client-lambda'
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'
import JSZip from 'jszip'
import type { NextRequest } from 'next/server'
import { z } from 'zod'
import { createLogger } from '@/lib/logs/console-logger'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
const logger = createLogger('AWSLambdaFetchAPI')
// Validation schema for the request body
const FetchRequestSchema = z.object({
accessKeyId: z.string().min(1, 'AWS Access Key ID is required'),
secretAccessKey: z.string().min(1, 'AWS Secret Access Key is required'),
region: z.string().min(1, 'AWS Region is required'),
functionName: z.string().min(1, 'Function name is required'),
})
type FetchRequest = z.infer<typeof FetchRequestSchema>
interface LambdaFunctionDetails {
functionArn: string
functionName: string
runtime: string
region: string
status: string
lastModified: string
codeSize: number
description: string
timeout: number
memorySize: number
environment: Record<string, string>
tags: Record<string, string>
codeFiles: Record<string, string>
handler: string
role: string
}
/**
* Extract code from Lambda function ZIP file
*/
async function extractCodeFromZip(zipBuffer: Buffer, runtime: string): Promise<{ mainCode: string; allFiles: Record<string, string> }> {
try {
const zip = await JSZip.loadAsync(zipBuffer)
// Log all files in the ZIP for debugging
const allFiles = Object.keys(zip.files)
console.log('Files in ZIP:', allFiles)
// Extract all text files
const allFilesContent: Record<string, string> = {}
let mainCode = ''
// Determine the main file based on runtime
let mainFile = 'index.js' // default
if (runtime.startsWith('python')) {
mainFile = 'index.py'
} else if (runtime.startsWith('java')) {
mainFile = 'index.java'
} else if (runtime.startsWith('dotnet')) {
mainFile = 'index.cs'
} else if (runtime.startsWith('go')) {
mainFile = 'index.go'
} else if (runtime.startsWith('ruby')) {
mainFile = 'index.rb'
}
console.log('Looking for main file:', mainFile)
// Extract all non-directory files
for (const fileName of allFiles) {
if (!fileName.endsWith('/')) {
try {
const fileContent = await zip.file(fileName)?.async('string')
if (fileContent !== undefined) {
allFilesContent[fileName] = fileContent
// Set main code if this is the main file
if (fileName === mainFile) {
mainCode = fileContent
console.log('Found main file content, length:', mainCode.length)
}
}
} catch (error) {
console.log(`Failed to extract file ${fileName}:`, error)
}
}
}
// If main file not found, try to find any code file
if (!mainCode) {
const codeFiles = Object.keys(allFilesContent).filter(file =>
file.endsWith('.js') || file.endsWith('.py') || file.endsWith('.java') ||
file.endsWith('.cs') || file.endsWith('.go') || file.endsWith('.rb')
)
console.log('Found code files:', codeFiles)
if (codeFiles.length > 0) {
const firstCodeFile = codeFiles[0]
mainCode = allFilesContent[firstCodeFile]
console.log('Using first code file as main, length:', mainCode.length)
}
}
// If still no main code, use the first file
if (!mainCode && Object.keys(allFilesContent).length > 0) {
const firstFile = Object.keys(allFilesContent)[0]
mainCode = allFilesContent[firstFile]
console.log('Using first file as main, length:', mainCode.length)
}
console.log(`Extracted ${Object.keys(allFilesContent).length} files`)
return { mainCode, allFiles: allFilesContent }
} catch (error) {
console.error('Failed to extract code from ZIP', { error })
return { mainCode: '', allFiles: {} }
}
}
/**
* Get detailed information about a Lambda function including code
*/
async function getFunctionDetailsWithCode(
lambdaClient: LambdaClient,
functionName: string,
region: string,
accessKeyId: string,
secretAccessKey: string
): Promise<LambdaFunctionDetails> {
// Get function configuration
const functionConfig = await lambdaClient.send(
new GetFunctionConfigurationCommand({ FunctionName: functionName })
)
// Get function code
const functionCode = await lambdaClient.send(
new GetFunctionCommand({ FunctionName: functionName })
)
let codeFiles: Record<string, string> = {}
if (functionCode.Code?.Location) {
try {
console.log('Downloading code from:', functionCode.Code.Location)
// Parse the S3 URL to extract bucket and key
const s3Url = new URL(functionCode.Code.Location)
const bucketName = s3Url.hostname.split('.')[0]
const objectKey = s3Url.pathname.substring(1) // Remove leading slash
console.log('Parsed S3 details:', { bucketName, objectKey })
// Create S3 client with the same credentials
const s3Client = new S3Client({
region: region,
credentials: {
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey,
},
})
// Download the object directly using AWS SDK
const getObjectCommand = new GetObjectCommand({
Bucket: bucketName,
Key: objectKey,
})
const s3Response = await s3Client.send(getObjectCommand)
if (s3Response.Body) {
// Convert the readable stream to buffer
const chunks: Uint8Array[] = []
const reader = s3Response.Body.transformToWebStream().getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
}
const zipBuffer = Buffer.concat(chunks)
console.log('ZIP buffer size:', zipBuffer.length)
const extractedCode = await extractCodeFromZip(zipBuffer, functionConfig.Runtime || '')
codeFiles = extractedCode.allFiles
console.log('Extracted files count:', Object.keys(codeFiles).length)
}
} catch (error) {
console.error('Failed to download function code using S3 SDK', { error })
// Fallback to fetch method if S3 SDK fails
try {
console.log('Trying fallback fetch method...')
const response = await fetch(functionCode.Code.Location)
console.log('Fetch response status:', response.status)
if (response.ok) {
const zipBuffer = Buffer.from(await response.arrayBuffer())
console.log('ZIP buffer size (fetch):', zipBuffer.length)
const extractedCode = await extractCodeFromZip(zipBuffer, functionConfig.Runtime || '')
codeFiles = extractedCode.allFiles
console.log('Extracted files count (fetch):', Object.keys(codeFiles).length)
} else {
console.log('Fetch failed with status:', response.status)
const errorText = await response.text()
console.log('Error response:', errorText)
}
} catch (fetchError) {
console.error('Fetch fallback also failed', { fetchError })
}
}
} else {
console.log('No code location found in function response')
}
return {
functionArn: functionConfig.FunctionArn || '',
functionName: functionConfig.FunctionName || '',
runtime: functionConfig.Runtime || '',
region,
status: functionConfig.State || '',
lastModified: functionConfig.LastModified || '',
codeSize: functionConfig.CodeSize || 0,
description: functionConfig.Description || '',
timeout: functionConfig.Timeout || 0,
memorySize: functionConfig.MemorySize || 0,
environment: functionConfig.Environment?.Variables || {},
tags: {}, // Tags need to be fetched separately if needed
codeFiles,
handler: functionConfig.Handler || '',
role: functionConfig.Role || '',
}
}
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
try {
logger.info(`[${requestId}] Processing AWS Lambda fetch request`)
// Parse and validate request body
let body: any
try {
body = await request.json()
} catch (parseError) {
logger.error(`[${requestId}] Failed to parse request body`, {
error: parseError instanceof Error ? parseError.message : String(parseError),
})
return createErrorResponse('Invalid JSON in request body', 400, 'INVALID_JSON')
}
const validationResult = FetchRequestSchema.safeParse(body)
if (!validationResult.success) {
logger.warn(`[${requestId}] Invalid request body`, { errors: validationResult.error.errors })
return createErrorResponse('Invalid request parameters', 400, 'VALIDATION_ERROR')
}
const params = validationResult.data
logger.info(`[${requestId}] Fetching Lambda function: ${params.functionName}`)
// Create Lambda client
const lambdaClient = new LambdaClient({
region: params.region,
credentials: {
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
},
})
// Fetch function details and code
const functionDetails = await getFunctionDetailsWithCode(
lambdaClient,
params.functionName,
params.region,
params.accessKeyId,
params.secretAccessKey
)
console.log('Final function details:', {
functionName: functionDetails.functionName,
filesCount: Object.keys(functionDetails.codeFiles).length,
hasFiles: Object.keys(functionDetails.codeFiles).length > 0,
allFields: Object.keys(functionDetails)
})
logger.info(`[${requestId}] Successfully fetched Lambda function: ${params.functionName}`)
// Return the response directly to see if createSuccessResponse is filtering fields
return new Response(JSON.stringify({
success: true,
output: functionDetails
}), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
})
} catch (error: any) {
logger.error(`[${requestId}] Failed to fetch Lambda function`, {
error: error.message,
stack: error.stack,
})
// Handle specific AWS errors
if (error.name === 'ResourceNotFoundException') {
let functionName = 'unknown'
try {
const errorBody = await request.json()
functionName = errorBody?.functionName || 'unknown'
} catch {
// Ignore parsing errors for error handling
}
return createErrorResponse(
`Lambda function '${functionName}' not found`,
404,
'FUNCTION_NOT_FOUND'
)
}
if (error.name === 'AccessDeniedException') {
return createErrorResponse(
'Access denied. Please check your AWS credentials and permissions.',
403,
'ACCESS_DENIED'
)
}
if (error.name === 'InvalidParameterValueException') {
return createErrorResponse(
'Invalid parameter value provided',
400,
'INVALID_PARAMETER'
)
}
return createErrorResponse(
'Failed to fetch Lambda function',
500,
'FETCH_ERROR'
)
}
}

View File

@@ -76,6 +76,24 @@ export const AWSLambdaBlock: BlockConfig<AWSLambdaResponse> = {
'sa-east-1',
],
},
{
id: 'operationType',
title: 'Operation Type',
type: 'dropdown',
layout: 'full',
options: ['fetch', 'create/update'],
},
{
id: 'fetchFunctionName',
title: 'Function Name',
type: 'short-input',
layout: 'full',
placeholder: 'Enter Lambda function name to fetch',
condition: {
field: 'operationType',
value: ['fetch'],
},
},
{
id: 'role',
title: 'Role ARN',
@@ -83,6 +101,10 @@ export const AWSLambdaBlock: BlockConfig<AWSLambdaResponse> = {
layout: 'full',
placeholder: 'Enter the IAM Role ARN for Lambda execution',
password: false,
condition: {
field: 'operationType',
value: ['create/update'],
},
},
{
id: 'functionName',
@@ -90,6 +112,10 @@ export const AWSLambdaBlock: BlockConfig<AWSLambdaResponse> = {
type: 'short-input',
layout: 'full',
placeholder: 'Enter Lambda function name',
condition: {
field: 'operationType',
value: ['create/update'],
},
},
{
id: 'runtime',
@@ -113,6 +139,10 @@ export const AWSLambdaBlock: BlockConfig<AWSLambdaResponse> = {
'provided.al2',
'provided',
],
condition: {
field: 'operationType',
value: ['create/update'],
},
},
{
id: 'handler',
@@ -121,21 +151,25 @@ export const AWSLambdaBlock: BlockConfig<AWSLambdaResponse> = {
layout: 'full',
placeholder: 'e.g., index.handler',
condition: {
field: 'runtime',
value: [
'nodejs18.x',
'nodejs16.x',
'nodejs14.x',
'python3.11',
'python3.10',
'python3.9',
'python3.8',
'java11',
'java8.al2',
'dotnet6',
'dotnetcore3.1',
'ruby2.7',
],
field: 'operationType',
value: ['create/update'],
and: {
field: 'runtime',
value: [
'nodejs18.x',
'nodejs16.x',
'nodejs14.x',
'python3.11',
'python3.10',
'python3.9',
'python3.8',
'java11',
'java8.al2',
'dotnet6',
'dotnetcore3.1',
'ruby2.7',
],
},
},
},
{
@@ -146,6 +180,10 @@ export const AWSLambdaBlock: BlockConfig<AWSLambdaResponse> = {
language: 'javascript',
generationType: 'javascript-function-body',
placeholder: '// Enter your Lambda function code here',
condition: {
field: 'operationType',
value: ['create/update'],
},
},
{
id: 'requirements',
@@ -156,8 +194,12 @@ export const AWSLambdaBlock: BlockConfig<AWSLambdaResponse> = {
placeholder:
'// Enter Python dependencies (requirements.txt format)\n// e.g., requests==2.31.0\n// boto3==1.34.0',
condition: {
field: 'runtime',
value: ['python3.11', 'python3.10', 'python3.9', 'python3.8'],
field: 'operationType',
value: ['create/update'],
and: {
field: 'runtime',
value: ['python3.11', 'python3.10', 'python3.9', 'python3.8'],
},
},
},
{
@@ -169,8 +211,12 @@ export const AWSLambdaBlock: BlockConfig<AWSLambdaResponse> = {
placeholder:
'{\n "name": "lambda-function",\n "version": "1.0.0",\n "dependencies": {\n "axios": "^1.6.0",\n "lodash": "^4.17.21"\n }\n}',
condition: {
field: 'runtime',
value: ['nodejs18.x', 'nodejs16.x', 'nodejs14.x'],
field: 'operationType',
value: ['create/update'],
and: {
field: 'runtime',
value: ['nodejs18.x', 'nodejs16.x', 'nodejs14.x'],
},
},
},
{
@@ -182,6 +228,10 @@ export const AWSLambdaBlock: BlockConfig<AWSLambdaResponse> = {
max: 900,
step: 1,
integer: true,
condition: {
field: 'operationType',
value: ['create/update'],
},
},
{
id: 'memorySize',
@@ -192,6 +242,10 @@ export const AWSLambdaBlock: BlockConfig<AWSLambdaResponse> = {
max: 10240,
step: 64,
integer: true,
condition: {
field: 'operationType',
value: ['create/update'],
},
},
{
id: 'environmentVariables',
@@ -200,6 +254,10 @@ export const AWSLambdaBlock: BlockConfig<AWSLambdaResponse> = {
layout: 'full',
columns: ['Key', 'Value'],
placeholder: 'Add environment variables as key-value pairs',
condition: {
field: 'operationType',
value: ['create/update'],
},
},
{
id: 'tags',
@@ -208,26 +266,44 @@ export const AWSLambdaBlock: BlockConfig<AWSLambdaResponse> = {
layout: 'full',
columns: ['Key', 'Value'],
placeholder: 'Add tags as key-value pairs',
condition: {
field: 'operationType',
value: ['create/update'],
},
},
],
tools: {
access: ['aws_lambda_deploy', 'aws_lambda_update', 'aws_lambda_invoke'],
access: ['aws_lambda_deploy', 'aws_lambda_update', 'aws_lambda_invoke', 'aws_lambda_fetch'],
config: {
tool: (params: Record<string, any>) => {
switch (params.operationType) {
case 'fetch':
return 'aws_lambda_fetch'
case 'create/update':
return 'aws_lambda_deploy'
default:
return 'aws_lambda_deploy' // Default to deploy
}
},
},
},
inputs: {
accessKeyId: { type: 'string', required: true },
secretAccessKey: { type: 'string', required: true },
region: { type: 'string', required: true },
role: { type: 'string', required: true },
functionName: { type: 'string', required: true },
operationType: { type: 'string', required: true },
fetchFunctionName: { type: 'string', required: false },
role: { type: 'string', required: false },
functionName: { type: 'string', required: false },
handler: { type: 'string', required: false },
runtime: { type: 'string', required: true },
code: { type: 'string', required: true },
runtime: { type: 'string', required: false },
code: { type: 'string', required: false },
requirements: { type: 'string', required: false },
packageJson: { type: 'string', required: false },
timeout: { type: 'number', required: true },
memorySize: { type: 'number', required: true },
environmentVariables: { type: 'json', required: true },
tags: { type: 'json', required: true },
timeout: { type: 'number', required: false },
memorySize: { type: 'number', required: false },
environmentVariables: { type: 'json', required: false },
tags: { type: 'json', required: false },
},
outputs: {
functionArn: 'string',

View File

@@ -0,0 +1,76 @@
import type { ToolConfig } from '../types'
interface AWSLambdaFetchInput {
accessKeyId: string
secretAccessKey: string
region: string
functionName?: string
fetchFunctionName?: string
}
interface AWSLambdaFetchOutput {
functionArn: string
functionName: string
runtime: string
region: string
status: string
lastModified: string
codeSize: number
description: string
timeout: number
memorySize: number
environment: Record<string, string>
tags: Record<string, string>
codeFiles: Record<string, string>
handler: string
role: string
}
export const awsLambdaFetchTool: ToolConfig<AWSLambdaFetchInput, AWSLambdaFetchOutput> = {
id: 'aws_lambda_fetch',
name: 'AWS Lambda Fetch',
description: 'Fetch AWS Lambda function details and code',
version: '1.0.0',
params: {
accessKeyId: {
type: 'string',
required: true,
description: 'AWS Access Key ID for authentication',
},
secretAccessKey: {
type: 'string',
required: true,
description: 'AWS Secret Access Key for authentication',
},
region: {
type: 'string',
required: true,
description: 'AWS region where the Lambda function is located',
},
functionName: {
type: 'string',
required: false,
description: 'Name of the Lambda function to fetch (legacy)',
},
fetchFunctionName: {
type: 'string',
required: false,
description: 'Name of the Lambda function to fetch',
},
},
request: {
url: '/api/tools/aws-lambda/fetch',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params: AWSLambdaFetchInput) => ({
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
region: params.region,
functionName: params.fetchFunctionName || params.functionName,
}),
},
}

View File

@@ -1 +1,2 @@
export { awsLambdaDeployTool } from './deploy'
export { awsLambdaFetchTool } from './fetch'

View File

@@ -5,7 +5,7 @@ import {
airtableUpdateRecordTool,
} from './airtable'
import { autoblocksPromptManagerTool } from './autoblocks'
import { awsLambdaDeployTool } from './aws_lambda'
import { awsLambdaDeployTool, awsLambdaFetchTool } from './aws_lambda'
import { browserUseRunTaskTool } from './browser_use'
import { clayPopulateTool } from './clay'
import { confluenceRetrieveTool, confluenceUpdateTool } from './confluence'
@@ -225,4 +225,5 @@ export const tools: Record<string, ToolConfig> = {
google_calendar_invite: googleCalendarInviteTool,
workflow_executor: workflowExecutorTool,
aws_lambda_deploy: awsLambdaDeployTool,
aws_lambda_fetch: awsLambdaFetchTool,
}