UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

1,806 lines (1,668 loc) 80.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.middyTemplate = void 0; exports.middyTemplate = { id: 'middy', name: 'middy', displayName: 'Middy AWS Lambda', description: 'Middleware engine for AWS Lambda with TypeScript, built-in middlewares, error handling, and AWS SDK integration', language: 'typescript', framework: 'middy', version: '5.2.0', tags: ['aws', 'lambda', 'serverless', 'middleware', 'typescript', 'api', 'cloud'], port: 3000, dependencies: {}, features: [ 'middleware-engine', 'aws-lambda', 'error-handling', 'validation', 'cors', 'authentication', 'logging', 'aws-sdk', 'serverless-framework', 'local-development' ], files: { // Package configuration 'package.json': `{ "name": "{{projectName}}", "version": "1.0.0", "description": "AWS Lambda functions with Middy middleware engine", "main": "dist/index.js", "scripts": { "dev": "serverless offline start --reloadHandler", "build": "tsc", "deploy": "serverless deploy", "deploy:prod": "serverless deploy --stage production", "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", "lint": "eslint src --ext .ts", "format": "prettier --write .", "typecheck": "tsc --noEmit", "logs": "serverless logs -f", "invoke:local": "serverless invoke local -f", "package": "serverless package", "remove": "serverless remove" }, "dependencies": { "@middy/core": "^5.2.0", "@middy/http-json-body-parser": "^5.2.0", "@middy/http-error-handler": "^5.2.0", "@middy/http-cors": "^5.2.0", "@middy/http-security-headers": "^5.2.0", "@middy/http-event-normalizer": "^5.2.0", "@middy/http-header-normalizer": "^5.2.0", "@middy/validator": "^5.2.0", "@middy/input-output-logger": "^5.2.0", "@middy/secrets-manager": "^5.2.0", "@middy/ssm": "^5.2.0", "@middy/warmup": "^5.2.0", "@middy/http-response-serializer": "^5.2.0", "@aws-sdk/client-dynamodb": "^3.540.0", "@aws-sdk/lib-dynamodb": "^3.540.0", "@aws-sdk/client-s3": "^3.540.0", "@aws-sdk/s3-request-presigner": "^3.540.0", "@aws-sdk/client-sqs": "^3.540.0", "@aws-sdk/client-sns": "^3.540.0", "@aws-sdk/client-secrets-manager": "^3.540.0", "@aws-sdk/client-ssm": "^3.540.0", "@aws-sdk/client-cloudwatch": "^3.540.0", "aws-lambda": "^1.0.7", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", "winston": "^3.13.0", "winston-cloudwatch": "^6.2.0", "uuid": "^9.0.1", "jsonwebtoken": "^9.0.2", "bcryptjs": "^2.4.3", "http-errors": "^2.0.0", "aws-xray-sdk-core": "^3.5.4" }, "devDependencies": { "@types/aws-lambda": "^8.10.136", "@types/node": "^20.12.7", "@types/jest": "^29.5.12", "@types/jsonwebtoken": "^9.0.6", "@types/bcryptjs": "^2.4.6", "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "prettier": "^3.2.5", "typescript": "^5.4.5", "jest": "^29.7.0", "ts-jest": "^29.1.2", "serverless": "^3.38.0", "serverless-offline": "^13.3.3", "serverless-plugin-typescript": "^2.1.5", "serverless-dotenv-plugin": "^6.0.0", "serverless-iam-roles-per-function": "^3.2.0", "serverless-plugin-aws-alerts": "^1.7.5", "serverless-prune-plugin": "^2.0.2", "serverless-plugin-tracing": "^2.0.0", "@serverless/typescript": "^3.30.1", "esbuild": "^0.20.2" } }`, // TypeScript configuration 'tsconfig.json': `{ "compilerOptions": { "target": "ES2022", "module": "commonjs", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "moduleResolution": "node", "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "declaration": true, "declarationMap": true, "sourceMap": true, "removeComments": true, "allowSyntheticDefaultImports": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist", "coverage", "**/*.test.ts"] }`, // Serverless configuration 'serverless.ts': `import type { AWS } from '@serverless/typescript'; const serverlessConfiguration: AWS = { service: '{{projectName}}', frameworkVersion: '3', plugins: [ 'serverless-plugin-typescript', 'serverless-offline', 'serverless-dotenv-plugin', 'serverless-iam-roles-per-function', 'serverless-plugin-aws-alerts', 'serverless-prune-plugin', 'serverless-plugin-tracing' ], provider: { name: 'aws', runtime: 'nodejs20.x', region: 'us-east-1', stage: '\${opt:stage, "dev"}', memorySize: 256, timeout: 30, tracing: { lambda: true, apiGateway: true }, environment: { NODE_ENV: '\${self:provider.stage}', REGION: '\${self:provider.region}', SERVICE_NAME: '\${self:service}', DYNAMODB_TABLE: '\${self:service}-\${self:provider.stage}-items', S3_BUCKET: '\${self:service}-\${self:provider.stage}-uploads', SQS_QUEUE_URL: { Ref: 'EventQueue' }, JWT_SECRET: '\${ssm:/\${self:service}/\${self:provider.stage}/jwt-secret}', LOG_LEVEL: '\${self:custom.logLevel.\${self:provider.stage}, "info"}' }, logs: { restApi: { accessLogging: true, executionLogging: true, level: 'INFO', fullExecutionData: true } }, apiGateway: { minimumCompressionSize: 1024, shouldStartNameWithService: true, metrics: true }, iam: { role: { statements: [ { Effect: 'Allow', Action: [ 'dynamodb:Query', 'dynamodb:Scan', 'dynamodb:GetItem', 'dynamodb:PutItem', 'dynamodb:UpdateItem', 'dynamodb:DeleteItem' ], Resource: [ { 'Fn::GetAtt': ['DynamoDBTable', 'Arn'] }, { 'Fn::Join': ['/', [{ 'Fn::GetAtt': ['DynamoDBTable', 'Arn'] }, 'index/*']] } ] }, { Effect: 'Allow', Action: [ 's3:GetObject', 's3:PutObject', 's3:DeleteObject', 's3:ListBucket' ], Resource: [ { 'Fn::GetAtt': ['S3Bucket', 'Arn'] }, { 'Fn::Join': ['', [{ 'Fn::GetAtt': ['S3Bucket', 'Arn'] }, '/*']] } ] }, { Effect: 'Allow', Action: [ 'sqs:SendMessage', 'sqs:ReceiveMessage', 'sqs:DeleteMessage', 'sqs:GetQueueAttributes' ], Resource: { 'Fn::GetAtt': ['EventQueue', 'Arn'] } }, { Effect: 'Allow', Action: [ 'ssm:GetParameter', 'ssm:GetParameters', 'ssm:GetParametersByPath' ], Resource: 'arn:aws:ssm:\${self:provider.region}:*:parameter/\${self:service}/\${self:provider.stage}/*' }, { Effect: 'Allow', Action: [ 'secretsmanager:GetSecretValue' ], Resource: 'arn:aws:secretsmanager:\${self:provider.region}:*:secret:\${self:service}/\${self:provider.stage}/*' }, { Effect: 'Allow', Action: [ 'logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents' ], Resource: 'arn:aws:logs:\${self:provider.region}:*:*' }, { Effect: 'Allow', Action: [ 'xray:PutTraceSegments', 'xray:PutTelemetryRecords' ], Resource: '*' } ] } } }, functions: { // HTTP API endpoints createItem: { handler: 'src/handlers/items.create', events: [ { http: { method: 'post', path: 'items', cors: true, authorizer: { name: 'authorizer', resultTtlInSeconds: 300 } } } ] }, getItem: { handler: 'src/handlers/items.get', events: [ { http: { method: 'get', path: 'items/{id}', cors: true, authorizer: { name: 'authorizer', resultTtlInSeconds: 300 } } } ] }, updateItem: { handler: 'src/handlers/items.update', events: [ { http: { method: 'put', path: 'items/{id}', cors: true, authorizer: { name: 'authorizer', resultTtlInSeconds: 300 } } } ] }, deleteItem: { handler: 'src/handlers/items.remove', events: [ { http: { method: 'delete', path: 'items/{id}', cors: true, authorizer: { name: 'authorizer', resultTtlInSeconds: 300 } } } ] }, listItems: { handler: 'src/handlers/items.list', events: [ { http: { method: 'get', path: 'items', cors: true, authorizer: { name: 'authorizer', resultTtlInSeconds: 300 } } } ] }, // Auth endpoints register: { handler: 'src/handlers/auth.register', events: [ { http: { method: 'post', path: 'auth/register', cors: true } } ] }, login: { handler: 'src/handlers/auth.login', events: [ { http: { method: 'post', path: 'auth/login', cors: true } } ] }, // Lambda authorizer authorizer: { handler: 'src/handlers/auth.authorize', environment: { JWT_SECRET: '\${ssm:/\${self:service}/\${self:provider.stage}/jwt-secret}' } }, // File upload uploadFile: { handler: 'src/handlers/files.upload', events: [ { http: { method: 'post', path: 'files/upload', cors: true, authorizer: { name: 'authorizer', resultTtlInSeconds: 300 } } } ] }, // SQS event processor processEvent: { handler: 'src/handlers/events.process', events: [ { sqs: { arn: { 'Fn::GetAtt': ['EventQueue', 'Arn'] }, batchSize: 10, maximumBatchingWindowInSeconds: 5 } } ], reservedConcurrency: 5 }, // Scheduled task cleanupTask: { handler: 'src/handlers/scheduled.cleanup', events: [ { schedule: { rate: 'rate(1 hour)', enabled: true } } ] }, // Health check health: { handler: 'src/handlers/health.check', events: [ { http: { method: 'get', path: 'health', cors: true } } ] } }, custom: { logLevel: { dev: 'debug', staging: 'info', production: 'warn' }, prune: { automatic: true, number: 3 }, alerts: { stages: ['staging', 'production'], topics: { alarm: '\${self:service}-\${opt:stage}-alerts-alarm', ok: '\${self:service}-\${opt:stage}-alerts-ok' }, alarms: [ { functionName: '\${self:service}-\${opt:stage}-createItem', metricName: 'Errors', threshold: 1, statistic: 'Sum', period: 60, evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: 'GreaterThanOrEqualToThreshold' } ] }, 'serverless-offline': { httpPort: 3000, lambdaPort: 3002, noPrependStageInUrl: true, useChildProcesses: true } }, resources: { Resources: { DynamoDBTable: { Type: 'AWS::DynamoDB::Table', Properties: { TableName: '\${self:provider.environment.DYNAMODB_TABLE}', AttributeDefinitions: [ { AttributeName: 'id', AttributeType: 'S' }, { AttributeName: 'userId', AttributeType: 'S' }, { AttributeName: 'createdAt', AttributeType: 'S' } ], KeySchema: [ { AttributeName: 'id', KeyType: 'HASH' } ], GlobalSecondaryIndexes: [ { IndexName: 'UserIdIndex', KeySchema: [ { AttributeName: 'userId', KeyType: 'HASH' }, { AttributeName: 'createdAt', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'ALL' }, ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1 } } ], BillingMode: 'PAY_PER_REQUEST', StreamSpecification: { StreamViewType: 'NEW_AND_OLD_IMAGES' }, PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: true }, SSESpecification: { SSEEnabled: true } } }, S3Bucket: { Type: 'AWS::S3::Bucket', Properties: { BucketName: '\${self:provider.environment.S3_BUCKET}', PublicAccessBlockConfiguration: { BlockPublicAcls: true, BlockPublicPolicy: true, IgnorePublicAcls: true, RestrictPublicBuckets: true }, BucketEncryption: { ServerSideEncryptionConfiguration: [ { ServerSideEncryptionByDefault: { SSEAlgorithm: 'AES256' } } ] }, LifecycleConfiguration: { Rules: [ { Id: 'DeleteOldFiles', Status: 'Enabled', ExpirationInDays: 30 } ] }, CorsConfiguration: { CorsRules: [ { AllowedHeaders: ['*'], AllowedMethods: ['GET', 'PUT', 'POST', 'DELETE'], AllowedOrigins: ['*'], MaxAge: 3000 } ] } } }, EventQueue: { Type: 'AWS::SQS::Queue', Properties: { QueueName: '\${self:service}-\${self:provider.stage}-events', VisibilityTimeout: 300, MessageRetentionPeriod: 1209600, ReceiveMessageWaitTimeSeconds: 20, RedrivePolicy: { deadLetterTargetArn: { 'Fn::GetAtt': ['EventDLQ', 'Arn'] }, maxReceiveCount: 3 } } }, EventDLQ: { Type: 'AWS::SQS::Queue', Properties: { QueueName: '\${self:service}-\${self:provider.stage}-events-dlq', MessageRetentionPeriod: 1209600 } }, GatewayResponseDefault4XX: { Type: 'AWS::ApiGateway::GatewayResponse', Properties: { ResponseParameters: { 'gatewayresponse.header.Access-Control-Allow-Origin': "'*'", 'gatewayresponse.header.Access-Control-Allow-Headers': "'*'" }, ResponseType: 'DEFAULT_4XX', RestApiId: { Ref: 'ApiGatewayRestApi' } } }, GatewayResponseDefault5XX: { Type: 'AWS::ApiGateway::GatewayResponse', Properties: { ResponseParameters: { 'gatewayresponse.header.Access-Control-Allow-Origin': "'*'", 'gatewayresponse.header.Access-Control-Allow-Headers': "'*'" }, ResponseType: 'DEFAULT_5XX', RestApiId: { Ref: 'ApiGatewayRestApi' } } } }, Outputs: { ApiGatewayUrl: { Description: 'API Gateway URL', Value: { 'Fn::Join': [ '', [ 'https://', { Ref: 'ApiGatewayRestApi' }, '.execute-api.\${self:provider.region}.amazonaws.com/\${self:provider.stage}' ] ] } }, DynamoDBTableName: { Description: 'DynamoDB Table Name', Value: { Ref: 'DynamoDBTable' } }, S3BucketName: { Description: 'S3 Bucket Name', Value: { Ref: 'S3Bucket' } }, EventQueueUrl: { Description: 'SQS Queue URL', Value: { Ref: 'EventQueue' } } } } }; module.exports = serverlessConfiguration;`, // Environment configuration '.env.example': `# Environment variables NODE_ENV=development LOG_LEVEL=debug # AWS Configuration (for local development) AWS_REGION=us-east-1 AWS_PROFILE=default # JWT Configuration JWT_SECRET=your-super-secret-jwt-key JWT_EXPIRES_IN=7d # Database DYNAMODB_ENDPOINT=http://localhost:8000 # S3 (for local development with LocalStack) S3_ENDPOINT=http://localhost:4566 # SQS (for local development with LocalStack) SQS_ENDPOINT=http://localhost:4566 # API Keys API_KEY=your-api-key # External Services SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL SENDGRID_API_KEY=your-sendgrid-api-key`, // Main handler with Middy middleware 'src/handlers/items.ts': `import middy from '@middy/core'; import jsonBodyParser from '@middy/http-json-body-parser'; import httpErrorHandler from '@middy/http-error-handler'; import cors from '@middy/http-cors'; import httpSecurityHeaders from '@middy/http-security-headers'; import httpEventNormalizer from '@middy/http-event-normalizer'; import httpHeaderNormalizer from '@middy/http-header-normalizer'; import validator from '@middy/validator'; import inputOutputLogger from '@middy/input-output-logger'; import { transpileSchema } from '@middy/validator/transpile'; import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; import { v4 as uuidv4 } from 'uuid'; import createError from 'http-errors'; import { logger } from '../utils/logger'; import { dynamoClient } from '../services/dynamodb'; import { authMiddleware } from '../middleware/auth'; import { errorLogger } from '../middleware/error-logger'; import { metricsMiddleware } from '../middleware/metrics'; import { rateLimiter } from '../middleware/rate-limiter'; import { sanitizer } from '../middleware/sanitizer'; import { cacheMiddleware } from '../middleware/cache'; // Input validation schemas const createItemSchema = { type: 'object', properties: { body: { type: 'object', properties: { name: { type: 'string', minLength: 1, maxLength: 100 }, description: { type: 'string', maxLength: 500 }, category: { type: 'string', enum: ['electronics', 'books', 'clothing', 'food', 'other'] }, price: { type: 'number', minimum: 0, maximum: 1000000 }, quantity: { type: 'integer', minimum: 0 }, tags: { type: 'array', items: { type: 'string' }, maxItems: 10 } }, required: ['name', 'category', 'price'], additionalProperties: false } } }; const updateItemSchema = { type: 'object', properties: { pathParameters: { type: 'object', properties: { id: { type: 'string', format: 'uuid' } }, required: ['id'] }, body: { type: 'object', properties: { name: { type: 'string', minLength: 1, maxLength: 100 }, description: { type: 'string', maxLength: 500 }, category: { type: 'string', enum: ['electronics', 'books', 'clothing', 'food', 'other'] }, price: { type: 'number', minimum: 0, maximum: 1000000 }, quantity: { type: 'integer', minimum: 0 }, tags: { type: 'array', items: { type: 'string' }, maxItems: 10 } }, additionalProperties: false } } }; // Create item handler const createItemHandler = async ( event: APIGatewayProxyEvent & { user?: any }, context: Context ): Promise<APIGatewayProxyResult> => { const id = uuidv4(); const timestamp = new Date().toISOString(); const item = { id, userId: event.user.id, ...event.body, createdAt: timestamp, updatedAt: timestamp }; try { await dynamoClient.putItem({ TableName: process.env.DYNAMODB_TABLE!, Item: item, ConditionExpression: 'attribute_not_exists(id)' }); logger.info('Item created successfully', { itemId: id, userId: event.user.id }); return { statusCode: 201, body: JSON.stringify({ message: 'Item created successfully', item }) }; } catch (error) { logger.error('Failed to create item', { error, userId: event.user.id }); throw createError(500, 'Failed to create item'); } }; // Get item handler const getItemHandler = async ( event: APIGatewayProxyEvent & { user?: any }, context: Context ): Promise<APIGatewayProxyResult> => { const { id } = event.pathParameters!; try { const result = await dynamoClient.getItem({ TableName: process.env.DYNAMODB_TABLE!, Key: { id } }); if (!result.Item) { throw createError(404, 'Item not found'); } // Check if user owns the item if (result.Item.userId !== event.user.id) { throw createError(403, 'Access denied'); } return { statusCode: 200, body: JSON.stringify({ item: result.Item }) }; } catch (error) { if (error.statusCode) throw error; logger.error('Failed to get item', { error, itemId: id }); throw createError(500, 'Failed to get item'); } }; // Update item handler const updateItemHandler = async ( event: APIGatewayProxyEvent & { user?: any }, context: Context ): Promise<APIGatewayProxyResult> => { const { id } = event.pathParameters!; const updates = event.body as any; try { // First, check if item exists and user owns it const existingItem = await dynamoClient.getItem({ TableName: process.env.DYNAMODB_TABLE!, Key: { id } }); if (!existingItem.Item) { throw createError(404, 'Item not found'); } if (existingItem.Item.userId !== event.user.id) { throw createError(403, 'Access denied'); } // Update item const updatedItem = { ...existingItem.Item, ...updates, id, // Ensure ID cannot be changed userId: event.user.id, // Ensure userId cannot be changed updatedAt: new Date().toISOString() }; await dynamoClient.putItem({ TableName: process.env.DYNAMODB_TABLE!, Item: updatedItem }); logger.info('Item updated successfully', { itemId: id, userId: event.user.id }); return { statusCode: 200, body: JSON.stringify({ message: 'Item updated successfully', item: updatedItem }) }; } catch (error) { if (error.statusCode) throw error; logger.error('Failed to update item', { error, itemId: id }); throw createError(500, 'Failed to update item'); } }; // Delete item handler const deleteItemHandler = async ( event: APIGatewayProxyEvent & { user?: any }, context: Context ): Promise<APIGatewayProxyResult> => { const { id } = event.pathParameters!; try { // First, check if item exists and user owns it const existingItem = await dynamoClient.getItem({ TableName: process.env.DYNAMODB_TABLE!, Key: { id } }); if (!existingItem.Item) { throw createError(404, 'Item not found'); } if (existingItem.Item.userId !== event.user.id) { throw createError(403, 'Access denied'); } // Delete item await dynamoClient.deleteItem({ TableName: process.env.DYNAMODB_TABLE!, Key: { id } }); logger.info('Item deleted successfully', { itemId: id, userId: event.user.id }); return { statusCode: 200, body: JSON.stringify({ message: 'Item deleted successfully' }) }; } catch (error) { if (error.statusCode) throw error; logger.error('Failed to delete item', { error, itemId: id }); throw createError(500, 'Failed to delete item'); } }; // List items handler const listItemsHandler = async ( event: APIGatewayProxyEvent & { user?: any }, context: Context ): Promise<APIGatewayProxyResult> => { const { limit = '20', lastKey } = event.queryStringParameters || {}; try { const result = await dynamoClient.query({ TableName: process.env.DYNAMODB_TABLE!, IndexName: 'UserIdIndex', KeyConditionExpression: 'userId = :userId', ExpressionAttributeValues: { ':userId': event.user.id }, Limit: parseInt(limit), ExclusiveStartKey: lastKey ? JSON.parse(Buffer.from(lastKey, 'base64').toString()) : undefined, ScanIndexForward: false // Sort by newest first }); const response: any = { items: result.Items || [], count: result.Count }; if (result.LastEvaluatedKey) { response.nextKey = Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString('base64'); } return { statusCode: 200, body: JSON.stringify(response) }; } catch (error) { logger.error('Failed to list items', { error, userId: event.user.id }); throw createError(500, 'Failed to list items'); } }; // Export handlers with Middy middleware export const create = middy(createItemHandler) .use(httpEventNormalizer()) .use(httpHeaderNormalizer()) .use(jsonBodyParser()) .use(validator({ eventSchema: transpileSchema(createItemSchema) })) .use(cors()) .use(httpSecurityHeaders()) .use(inputOutputLogger({ logger })) .use(authMiddleware()) .use(sanitizer()) .use(rateLimiter({ maxRequests: 100, windowMs: 60000 })) .use(metricsMiddleware()) .use(errorLogger()) .use(httpErrorHandler()); export const get = middy(getItemHandler) .use(httpEventNormalizer()) .use(httpHeaderNormalizer()) .use(cors()) .use(httpSecurityHeaders()) .use(inputOutputLogger({ logger })) .use(authMiddleware()) .use(cacheMiddleware({ ttl: 300 })) .use(metricsMiddleware()) .use(errorLogger()) .use(httpErrorHandler()); export const update = middy(updateItemHandler) .use(httpEventNormalizer()) .use(httpHeaderNormalizer()) .use(jsonBodyParser()) .use(validator({ eventSchema: transpileSchema(updateItemSchema) })) .use(cors()) .use(httpSecurityHeaders()) .use(inputOutputLogger({ logger })) .use(authMiddleware()) .use(sanitizer()) .use(rateLimiter({ maxRequests: 50, windowMs: 60000 })) .use(metricsMiddleware()) .use(errorLogger()) .use(httpErrorHandler()); export const remove = middy(deleteItemHandler) .use(httpEventNormalizer()) .use(httpHeaderNormalizer()) .use(cors()) .use(httpSecurityHeaders()) .use(inputOutputLogger({ logger })) .use(authMiddleware()) .use(rateLimiter({ maxRequests: 20, windowMs: 60000 })) .use(metricsMiddleware()) .use(errorLogger()) .use(httpErrorHandler()); export const list = middy(listItemsHandler) .use(httpEventNormalizer()) .use(httpHeaderNormalizer()) .use(cors()) .use(httpSecurityHeaders()) .use(inputOutputLogger({ logger })) .use(authMiddleware()) .use(cacheMiddleware({ ttl: 60 })) .use(metricsMiddleware()) .use(errorLogger()) .use(httpErrorHandler());`, // Authentication handlers 'src/handlers/auth.ts': `import middy from '@middy/core'; import jsonBodyParser from '@middy/http-json-body-parser'; import httpErrorHandler from '@middy/http-error-handler'; import cors from '@middy/http-cors'; import httpSecurityHeaders from '@middy/http-security-headers'; import httpEventNormalizer from '@middy/http-event-normalizer'; import httpHeaderNormalizer from '@middy/http-header-normalizer'; import validator from '@middy/validator'; import { transpileSchema } from '@middy/validator/transpile'; import ssm from '@middy/ssm'; import { APIGatewayProxyEvent, APIGatewayProxyResult, APIGatewayAuthorizerResult, Context } from 'aws-lambda'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import { v4 as uuidv4 } from 'uuid'; import createError from 'http-errors'; import { logger } from '../utils/logger'; import { dynamoClient } from '../services/dynamodb'; import { errorLogger } from '../middleware/error-logger'; import { sanitizer } from '../middleware/sanitizer'; // Validation schemas const registerSchema = { type: 'object', properties: { body: { type: 'object', properties: { email: { type: 'string', format: 'email' }, password: { type: 'string', minLength: 8, maxLength: 100 }, name: { type: 'string', minLength: 1, maxLength: 100 } }, required: ['email', 'password', 'name'], additionalProperties: false } } }; const loginSchema = { type: 'object', properties: { body: { type: 'object', properties: { email: { type: 'string', format: 'email' }, password: { type: 'string' } }, required: ['email', 'password'], additionalProperties: false } } }; // Register handler const registerHandler = async ( event: APIGatewayProxyEvent & { secrets?: any }, context: Context ): Promise<APIGatewayProxyResult> => { const { email, password, name } = event.body as any; try { // Check if user already exists const existingUser = await dynamoClient.query({ TableName: process.env.DYNAMODB_TABLE!, IndexName: 'EmailIndex', KeyConditionExpression: 'email = :email', ExpressionAttributeValues: { ':email': email } }); if (existingUser.Items && existingUser.Items.length > 0) { throw createError(409, 'User already exists'); } // Hash password const hashedPassword = await bcrypt.hash(password, 10); // Create user const userId = uuidv4(); const timestamp = new Date().toISOString(); const user = { id: userId, email, password: hashedPassword, name, createdAt: timestamp, updatedAt: timestamp, emailVerified: false, isActive: true }; await dynamoClient.putItem({ TableName: process.env.DYNAMODB_TABLE!, Item: user }); // Generate JWT token const token = jwt.sign( { id: userId, email }, event.secrets.jwtSecret || process.env.JWT_SECRET!, { expiresIn: process.env.JWT_EXPIRES_IN || '7d' } ); logger.info('User registered successfully', { userId, email }); return { statusCode: 201, body: JSON.stringify({ message: 'User registered successfully', user: { id: userId, email, name }, token }) }; } catch (error) { if (error.statusCode) throw error; logger.error('Failed to register user', { error, email }); throw createError(500, 'Failed to register user'); } }; // Login handler const loginHandler = async ( event: APIGatewayProxyEvent & { secrets?: any }, context: Context ): Promise<APIGatewayProxyResult> => { const { email, password } = event.body as any; try { // Find user by email const result = await dynamoClient.query({ TableName: process.env.DYNAMODB_TABLE!, IndexName: 'EmailIndex', KeyConditionExpression: 'email = :email', ExpressionAttributeValues: { ':email': email } }); if (!result.Items || result.Items.length === 0) { throw createError(401, 'Invalid credentials'); } const user = result.Items[0]; // Check if user is active if (!user.isActive) { throw createError(403, 'Account is disabled'); } // Verify password const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { throw createError(401, 'Invalid credentials'); } // Generate JWT token const token = jwt.sign( { id: user.id, email: user.email }, event.secrets.jwtSecret || process.env.JWT_SECRET!, { expiresIn: process.env.JWT_EXPIRES_IN || '7d' } ); // Update last login await dynamoClient.updateItem({ TableName: process.env.DYNAMODB_TABLE!, Key: { id: user.id }, UpdateExpression: 'SET lastLogin = :timestamp', ExpressionAttributeValues: { ':timestamp': new Date().toISOString() } }); logger.info('User logged in successfully', { userId: user.id, email }); return { statusCode: 200, body: JSON.stringify({ message: 'Login successful', user: { id: user.id, email: user.email, name: user.name }, token }) }; } catch (error) { if (error.statusCode) throw error; logger.error('Failed to login user', { error, email }); throw createError(500, 'Failed to login'); } }; // Lambda authorizer export const authorize = async ( event: any, context: Context ): Promise<APIGatewayAuthorizerResult> => { const token = event.authorizationToken?.replace('Bearer ', ''); if (!token) { throw new Error('Unauthorized'); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any; // Get user from database to check if still active const result = await dynamoClient.getItem({ TableName: process.env.DYNAMODB_TABLE!, Key: { id: decoded.id } }); if (!result.Item || !result.Item.isActive) { throw new Error('Unauthorized'); } return { principalId: decoded.id, policyDocument: { Version: '2012-10-17', Statement: [ { Action: 'execute-api:Invoke', Effect: 'Allow', Resource: event.methodArn } ] }, context: { userId: decoded.id, email: decoded.email } }; } catch (error) { logger.error('Authorization failed', { error, token: token.substring(0, 10) + '...' }); throw new Error('Unauthorized'); } }; // Export handlers with Middy middleware export const register = middy(registerHandler) .use(httpEventNormalizer()) .use(httpHeaderNormalizer()) .use(jsonBodyParser()) .use(validator({ eventSchema: transpileSchema(registerSchema) })) .use(cors()) .use(httpSecurityHeaders()) .use(sanitizer()) .use(ssm({ fetchData: { jwtSecret: '\${ssm:/\${self:service}/\${self:provider.stage}/jwt-secret}' }, setToContext: false, setToEnv: false })) .use(errorLogger()) .use(httpErrorHandler()); export const login = middy(loginHandler) .use(httpEventNormalizer()) .use(httpHeaderNormalizer()) .use(jsonBodyParser()) .use(validator({ eventSchema: transpileSchema(loginSchema) })) .use(cors()) .use(httpSecurityHeaders()) .use(sanitizer()) .use(ssm({ fetchData: { jwtSecret: '\${ssm:/\${self:service}/\${self:provider.stage}/jwt-secret}' }, setToContext: false, setToEnv: false })) .use(errorLogger()) .use(httpErrorHandler());`, // Custom middleware - Authentication 'src/middleware/auth.ts': `import jwt from 'jsonwebtoken'; import createError from 'http-errors'; import { logger } from '../utils/logger'; export const authMiddleware = () => ({ before: async (request: any) => { const { event } = request; const token = event.headers?.Authorization?.replace('Bearer ', '') || event.headers?.authorization?.replace('Bearer ', ''); if (!token) { throw createError(401, 'No token provided'); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any; // Add user info to event event.user = { id: decoded.id, email: decoded.email }; // Add user context to Lambda context if (event.requestContext) { event.requestContext.authorizer = { principalId: decoded.id, ...decoded }; } } catch (error) { logger.error('Invalid token', { error }); throw createError(401, 'Invalid token'); } } });`, // Custom middleware - Error Logger 'src/middleware/error-logger.ts': `import { logger } from '../utils/logger'; export const errorLogger = () => ({ onError: async (request: any) => { const { error, event, context } = request; // Log error details logger.error('Lambda execution error', { error: { name: error.name, message: error.message, stack: error.stack, statusCode: error.statusCode }, event: { path: event.path, method: event.httpMethod, headers: event.headers, queryStringParameters: event.queryStringParameters, pathParameters: event.pathParameters }, context: { requestId: context.requestId, functionName: context.functionName, remainingTimeInMillis: context.getRemainingTimeInMillis() } }); // Don't swallow the error return request.error; } });`, // Custom middleware - Metrics 'src/middleware/metrics.ts': `import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch'; const cloudwatch = new CloudWatchClient({ region: process.env.AWS_REGION }); export const metricsMiddleware = () => { let startTime: number; return { before: async (request: any) => { startTime = Date.now(); }, after: async (request: any) => { const duration = Date.now() - startTime; const { event, context } = request; try { await cloudwatch.send(new PutMetricDataCommand({ Namespace: 'Lambda/Performance', MetricData: [ { MetricName: 'Duration', Value: duration, Unit: 'Milliseconds', Dimensions: [ { Name: 'FunctionName', Value: context.functionName }, { Name: 'Path', Value: event.path || 'unknown' } ] }, { MetricName: 'SuccessCount', Value: 1, Unit: 'Count', Dimensions: [ { Name: 'FunctionName', Value: context.functionName } ] } ] })); } catch (error) { // Don't fail the request if metrics fail console.error('Failed to send metrics', error); } }, onError: async (request: any) => { const { context, error } = request; try { await cloudwatch.send(new PutMetricDataCommand({ Namespace: 'Lambda/Performance', MetricData: [ { MetricName: 'ErrorCount', Value: 1, Unit: 'Count', Dimensions: [ { Name: 'FunctionName', Value: context.functionName }, { Name: 'ErrorType', Value: error.name || 'Unknown' } ] } ] })); } catch (metricsError) { console.error('Failed to send error metrics', metricsError); } } }; };`, // Custom middleware - Rate Limiter 'src/middleware/rate-limiter.ts': `import createError from 'http-errors'; import { logger } from '../utils/logger'; interface RateLimiterOptions { maxRequests: number; windowMs: number; } // In-memory store for rate limiting (use Redis in production) const requestCounts = new Map<string, { count: number; resetTime: number }>(); export const rateLimiter = (options: RateLimiterOptions) => ({ before: async (request: any) => { const { event } = request; const now = Date.now(); // Get client identifier (IP or user ID) const clientId = event.user?.id || event.requestContext?.identity?.sourceIp || 'anonymous'; const clientData = requestCounts.get(clientId); if (!clientData || now > clientData.resetTime) { // New window requestCounts.set(clientId, { count: 1, resetTime: now + options.windowMs }); } else { // Existing window clientData.count++; if (clientData.count > options.maxRequests) { logger.warn('Rate limit exceeded', { clientId, count: clientData.count, limit: options.maxRequests }); throw createError(429, 'Too many requests', { headers: { 'Retry-After': Math.ceil((clientData.resetTime - now) / 1000).toString(), 'X-RateLimit-Limit': options.maxRequests.toString(), 'X-RateLimit-Remaining': '0', 'X-RateLimit-Reset': new Date(clientData.resetTime).toISOString() } }); } } // Add rate limit headers to response if (!request.response) { request.response = {}; } if (!request.response.headers) { request.response.headers = {}; } const remaining = options.maxRequests - (clientData?.count || 1); request.response.headers['X-RateLimit-Limit'] = options.maxRequests.toString(); request.response.headers['X-RateLimit-Remaining'] = remaining.toString(); request.response.headers['X-RateLimit-Reset'] = new Date( clientData?.resetTime || now + options.windowMs ).toISOString(); } });`, // Custom middleware - Sanitizer 'src/middleware/sanitizer.ts': `export const sanitizer = () => ({ before: async (request: any) => { const { event } = request; // Sanitize body if (event.body && typeof event.body === 'object') { event.body = sanitizeObject(event.body); } // Sanitize query parameters if (event.queryStringParameters) { event.queryStringParameters = sanitizeObject(event.queryStringParameters); } // Sanitize path parameters if (event.pathParameters) { event.pathParameters = sanitizeObject(event.pathParameters); } } }); function sanitizeObject(obj: any): any { if (typeof obj !== 'object' || obj === null) { return sanitizeValue(obj); } if (Array.isArray(obj)) { return obj.map(sanitizeObject); } const sanitized: any = {}; for (const [key, value] of Object.entries(obj)) { sanitized[key] = sanitizeObject(value); } return sanitized; } function sanitizeValue(value: any): any { if (typeof value !== 'string') { return value; } // Remove potential XSS patterns let sanitized = value .replace(/<script[^>]*>.*?<\/script>/gi, '') .replace(/<iframe[^>]*>.*?<\/iframe>/gi, '') .replace(/javascript:/gi, '') .replace(/on\w+\s*=/gi, ''); // Remove SQL injection patterns sanitized = sanitized .replace(/(\b(union|select|insert|update|delete|drop|create|alter|exec|execute)\b)/gi, '') .replace(/[';]--/g, '') .replace(/\/\*.*?\*\//g, ''); // Trim whitespace return sanitized.trim(); }`, // Custom middleware - Cache 'src/middleware/cache.ts': `import { logger } from '../utils/logger'; interface CacheOptions { ttl: number; // Time to live in seconds } // Simple in-memory cache (use Redis or ElastiCache in production) const cache = new Map<string, { data: any; expires: number }>(); export const cacheMiddleware = (options: CacheOptions) => ({ before: async (request: any) => { const { event } = request; // Only cache GET requests if (event.httpMethod !== 'GET') { return; } const cacheKey = generateCacheKey(event); const cached = cache.get(cacheKey); if (cached && cached.expires > Date.now()) { logger.info('Cache hit', { cacheKey }); // Return cached response request.response = { statusCode: 200, headers: { 'X-Cache': 'HIT', 'Cache-Control': \`public, max-age=\${options.ttl}\` }, body: cached.data }; return request.response; } }, after: async (request: any) => { const { event, response } = request; // Only cache successful GET requests if (event.httpMethod !== 'GET' || response.statusCode !== 200) { return; } const cacheKey = generateCacheKey(event); const expires = Date.now() + (options.ttl * 1000); cache.set(cacheKey, { data: response.body, expires }); // Add cache headers if (!response.headers) { response.headers = {}; } response.headers['X-Cache'] = 'MISS'; response.headers['Cache-Control'] = \`public, max-age=\${options.ttl}\`; logger.info('Response cached', { cacheKey, ttl: options.ttl }); } }); function generateCacheKey(event: any): string { const parts = [ event.path, event.httpMethod, JSON.stringify(event.queryStringParameters || {}), event.user?.id || 'anonymous' ]; return parts.join(':'); }`, // DynamoDB service 'src/services/dynamodb.ts': `import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient, PutCommand, GetCommand, UpdateCommand, DeleteCommand, QueryCommand, ScanCommand } from '@aws-sdk/lib-dynamodb'; import { logger } from '../utils/logger'; const client = new DynamoDBClient({ region: process.env.AWS_REGION, ...(process.env.DYNAMODB_ENDPOINT && { endpoint: process.env.DYNAMODB_ENDPOINT }) }); const docClient = DynamoDBDocumentClient.from(client, { marshallOptions: { removeUndefinedValues: true, convertClassInstanceToMap: true } }); export const dynamoClient = { async putItem(params: any) { try { const command = new PutCommand(params); const result = await docClient.send(command); return result; } catch (error) { logger.error('DynamoDB putItem error', { error, params }); throw error; } }, async getItem(params: any) { try { const command = new GetCommand(params); const result = await docClient.send(command); return result; } catch (error) { logger.error('DynamoDB getItem error', { error, params }); throw error; } }, async updateItem(params: any) { try { const command = new UpdateCommand(params); const result = await docClient.send(command); return result; } catch (error) { logger.error('DynamoDB updateItem error', { error, params }); throw error; } }, async deleteItem(params: any) { try { const command = new DeleteCommand(params); const result = await docClient.send(command); return result; } catch (error) { logger.error('DynamoDB deleteItem error', { error, params }); throw error; } }, async query(params: any) { try { const command = new QueryCommand(params); const result = await docClient.send(command); return result; } catch (error) { logger.error('DynamoDB query error', { error, params }); throw error; } }, async scan(params: any) { try { const command = new ScanCommand(params); const result = await docClient.send(command); return result; } catch (error) { logger.error('DynamoDB scan error', { error, params }); throw error; } } };`, // S3 service 'src/services/s3.ts': `import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { logger } from '../utils/logger'; const client = new S3Client({ region: process.env.AWS_REGION, ...(process.env.S3_ENDPOINT && { endpoint: process.env.S3_ENDPOINT, forcePathStyle: true }) }); export const s3Client = { async putObject(bucket: string, key: string, body: any, contentType?: string) { try { const command = new PutObjectCommand({ Bucket: bucket, Key: key, Body: body, ContentType: contentType }); const result = await client.send(command); return result; } catch (error) { logger.error('S3 putObject error', { error, bucket, key }); throw error; } }, async getObject(bucket: string, key: string) { try { const command = new GetObjectCommand({ Bucket: bucket, Key: key }); const result = await client.send(command); return result; } catch (error) { logger.error('S3 getObject error', { error, bucket, key }); throw error; } }, async deleteObject(bucket: string, key: string) { try { const command = new DeleteObjectCommand({ Bucket: bucket, Key: key }); const result = await client.send(command); return result; } catch (error) { logger.error('S3 deleteObject error', { error, bucket, key }); throw error; } }, async listObjects(bucket: string, prefix?: string, maxKeys?: nu