UNPKG

cdk-serverless-agentic-api

Version:

CDK construct for serverless web applications with CloudFront, S3, Cognito, API Gateway, and Lambda

340 lines 14.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.CDKServerlessAgenticAPI = void 0; const constructs_1 = require("constructs"); const s3 = __importStar(require("aws-cdk-lib/aws-s3")); const cognito = __importStar(require("aws-cdk-lib/aws-cognito")); const apigateway = __importStar(require("aws-cdk-lib/aws-apigateway")); const iam = __importStar(require("aws-cdk-lib/aws-iam")); const security_validation_1 = require("./security-validation"); const s3_1 = require("./s3"); const cognito_1 = require("./cognito"); const api_gateway_1 = require("./api-gateway"); const lambda_1 = require("./lambda"); const cloudfront_1 = require("./cloudfront"); const monitoring_1 = require("./monitoring"); const path = __importStar(require("path")); /** * CDK construct that creates a complete serverless web application infrastructure * including CloudFront, S3, Cognito, API Gateway, and Lambda functions. */ class CDKServerlessAgenticAPI extends constructs_1.Construct { /** * Gets the Cognito User Pool Client for API Gateway integration */ get userPoolClient() { return this._userPoolClient; } /** * Creates a new CDKServerlessAgenticAPI * * @param scope The parent construct * @param id The construct ID * @param props Configuration properties */ constructor(scope, id, props) { super(scope, id); // Initialize internal state this.lambdaFunctions = {}; this.resourceConfigs = {}; // Store props in node context for use in other methods this.node.setContext('props', props); // Validate props if (props?.domainName && !props?.certificateArn) { throw new Error('certificateArn is required when domainName is provided'); } // Extension mode - use existing resources or create new ones if (props?.extensionMode?.apiId) { this.api = apigateway.RestApi.fromRestApiId(this, 'ImportedApi', props.extensionMode.apiId); } else { this.api = (0, api_gateway_1.createApiGateway)(this, id, props); } if (props?.extensionMode?.userPoolId) { this.userPool = cognito.UserPool.fromUserPoolId(this, 'ImportedUserPool', props.extensionMode.userPoolId); this._userPoolClient = cognito.UserPoolClient.fromUserPoolClientId(this, 'ImportedUserPoolClient', props.extensionMode.userPoolClientId); } else { const cognitoResources = (0, cognito_1.createUserPool)(this, id, props); this.userPool = cognitoResources.userPool; this._userPoolClient = cognitoResources.userPoolClient; } if (props?.extensionMode?.cognitoAuthorizerId) { // Create a new authorizer reference for extension mode this.cognitoAuthorizer = new apigateway.CfnAuthorizer(this, 'ImportedAuthorizer', { restApiId: this.api.restApiId, type: 'COGNITO_USER_POOLS', identitySource: 'method.request.header.Authorization', name: 'CognitoAuthorizer', providerArns: [this.userPool.userPoolArn] }); // Override the ref to use the imported ID this.cognitoAuthorizer.ref = props.extensionMode.cognitoAuthorizerId; } else { this.cognitoAuthorizer = (0, api_gateway_1.createCognitoAuthorizer)(this, this.api, this.userPool, id); } // Create S3 bucket only if not skipped if (!props?.skipResources?.skipBucket) { if (props?.extensionMode?.bucketName) { this.bucket = s3.Bucket.fromBucketName(this, 'ImportedBucket', props.extensionMode.bucketName); } else { this.bucket = (0, s3_1.createS3Bucket)(this, id, props); } // Create CloudFront Origin Access Identity for secure S3 access this.originAccessIdentity = (0, s3_1.createOriginAccessIdentity)(this, id); // Configure S3 bucket policy for CloudFront access if (this.bucket && this.originAccessIdentity) { (0, s3_1.configureBucketPolicy)(this.bucket, this.originAccessIdentity); } } // Create logging bucket if logging is enabled and not skipped const loggingBucket = (props?.enableLogging !== false && !props?.skipResources?.skipLoggingBucket) ? (0, s3_1.createLoggingBucket)(this) : undefined; // Create CloudFront distribution only if not skipped if (!props?.skipResources?.skipDistribution) { if (props?.extensionMode?.distributionId) { // Note: CloudFront Distribution cannot be imported, so we'll set it to undefined // Extension stacks should not need to reference the distribution directly this.distribution = undefined; } else if (this.bucket && this.originAccessIdentity) { this.distribution = (0, cloudfront_1.createCloudFrontDistribution)(this, id, this.bucket, this.originAccessIdentity, this.api, props, loggingBucket); } } // Create default endpoints first to populate lambdaFunctions registry (unless skipped) if (!props?.skipResources?.skipDefaultEndpoints) { this.createDefaultEndpoints(); } // Configure monitoring and alarms if logging is enabled // This must come after createDefaultEndpoints() to avoid circular dependency if (props?.enableLogging !== false && this.distribution) { (0, monitoring_1.createMonitoringResources)(this, this.api, this.lambdaFunctions, this.distribution, id); } } /** * Creates default health, whoami, and config endpoints */ createDefaultEndpoints() { // Determine the base lambda source path const baseLambdaPath = this.node.tryGetContext('props')?.lambdaSourcePath || path.join(__dirname, '../lambda'); // Create health endpoint this.addResource({ path: '/health', method: 'GET', lambdaSourcePath: path.join(baseLambdaPath, 'health'), requiresAuth: false, environment: { API_VERSION: '1.0.0', SERVICE_NAME: 'serverless-web-app-api' } }); // Create whoami endpoint this.addResource({ path: '/whoami', method: 'GET', lambdaSourcePath: path.join(baseLambdaPath, 'whoami'), requiresAuth: true, environment: { API_VERSION: '1.0.0', SERVICE_NAME: 'serverless-web-app-api' } }); // Create config endpoint for frontend configuration // Note: Cognito values are now looked up dynamically to avoid circular dependencies const configLambda = this.addResource({ path: '/config', method: 'GET', lambdaSourcePath: path.join(baseLambdaPath, 'config'), requiresAuth: false, environment: { API_VERSION: '1.0.0' } }); // Add permissions to list Cognito resources configLambda.addToRolePolicy(new iam.PolicyStatement({ sid: 'CognitoListAccess', effect: iam.Effect.ALLOW, actions: [ 'cognito-idp:ListUserPools', 'cognito-idp:ListUserPoolClients' ], resources: ['*'] // Scope is limited to listing operations only })); } /** * Adds a new API resource with an associated Lambda function * * @param options Configuration for the new resource * @returns The created Lambda function */ addResource(options) { // Validate input parameters this.validateAddResourceOptions(options); // Create resource configuration const config = { path: `/api${options.path}`, method: options.method || 'GET', requiresAuth: options.requiresAuth || false, cognitoGroup: options.cognitoGroup, lambdaSourcePath: options.lambdaSourcePath, environment: options.environment, enableDLQ: options.enableDLQ || false, enableHealthAlarms: options.enableHealthAlarms || false }; // Store configuration for later implementation const resourceKey = `${config.method}:${config.path}`; this.resourceConfigs[resourceKey] = config; // Create Lambda function for this resource const lambdaFunction = (0, lambda_1.createApiLambdaFunction)(this, config.path, config, this.userPool, this.userPoolClient, this.node.id); // Create API Gateway resource and method const apiResource = (0, api_gateway_1.createApiGatewayResource)(this.api, config.path); // eslint-disable-next-line @typescript-eslint/no-unused-vars (0, api_gateway_1.createApiGatewayMethod)(apiResource, config, lambdaFunction, this.cognitoAuthorizer, this.api); // Store Lambda function in registry const lambdaEntry = { function: lambdaFunction, config: config }; this.lambdaFunctions[`${config.method} ${config.path}`] = lambdaEntry; return lambdaFunction; } /** * Validates the options provided to addResource method * * @param options The options to validate * @throws Error if validation fails */ validateAddResourceOptions(options) { if (!options.path) { throw new Error('Resource path is required'); } if (!options.path.startsWith('/')) { throw new Error('Resource path must start with "/"'); } if (!options.lambdaSourcePath) { throw new Error('Lambda source path is required'); } if (options.method && !['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'].includes(options.method.toUpperCase())) { throw new Error('Invalid HTTP method. Supported methods: GET, POST, PUT, DELETE, PATCH, OPTIONS'); } if (options.cognitoGroup && !options.requiresAuth) { throw new Error('cognitoGroup can only be specified when requiresAuth is true'); } } /** * Creates a Lambda function from a source directory with proper IAM role and configuration * * @param functionName Unique name for the Lambda function * @param sourcePath Path to the directory containing the Lambda function source code * @param environment Environment variables to pass to the Lambda function * @param additionalPolicies Additional IAM policies to attach to the Lambda execution role * @returns The created Lambda function */ createLambdaFunction(functionName, sourcePath, environment, additionalPolicies) { return (0, lambda_1.createLambdaFunction)(this, functionName, sourcePath, this.node.id, environment, additionalPolicies, false); } /** * Validates the security configuration of the construct * * @param options Security validation options * @returns Array of validation results */ validateSecurity(options = {}) { const defaultOptions = { throwOnFailure: false, logResults: true }; return (0, security_validation_1.validateSecurityConfiguration)(this, { ...defaultOptions, ...options }); } /** * Enforces security best practices for the construct * * @param options Security enforcement options */ enforceSecurityBestPractices(options = {}) { (0, security_validation_1.enforceSecurityBestPractices)(this, options); } /** * Gets a Lambda function by path and method * * @param path The API path (e.g., '/users') * @param method The HTTP method (defaults to 'GET') * @returns The Lambda function or undefined if not found */ getLambdaFunction(path, method = 'GET') { const key = `${method.toUpperCase()} /api${path}`; return this.lambdaFunctions[key]?.function; } /** * Gets exportable resource IDs for use in extension stacks * * @returns Object containing resource IDs that can be used by extension stacks */ getExportableResourceIds() { return { apiId: this.api.restApiId, userPoolId: this.userPool.userPoolId, userPoolClientId: this.userPoolClient.userPoolClientId, bucketName: this.bucket?.bucketName, distributionId: this.distribution?.distributionId, cognitoAuthorizerId: this.cognitoAuthorizer.ref }; } /** * Grants DynamoDB access to a Lambda function * * @param lambdaFunction The Lambda function to grant access to * @param table The DynamoDB table to grant access to * @param accessType The type of access to grant ('read', 'write', or 'readwrite') */ grantDynamoDBAccess(lambdaFunction, table, accessType = 'readwrite') { switch (accessType) { case 'read': table.grantReadData(lambdaFunction); break; case 'write': table.grantWriteData(lambdaFunction); break; case 'readwrite': table.grantReadWriteData(lambdaFunction); break; } } } exports.CDKServerlessAgenticAPI = CDKServerlessAgenticAPI; //# sourceMappingURL=cdk-serverless-agentic-api.js.map