import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as kms from 'aws-cdk-lib/aws-kms'; import * as ssm from 'aws-cdk-lib/aws-ssm'; import * as events from 'aws-cdk-lib/aws-events'; import * as targets from 'aws-cdk-lib/aws-events-targets'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as path from 'path'; export interface PoSyncProps { region: string; kbDocsBucket: s3.Bucket; knowledgeBaseId: string; dataSourceId: string; } export class PoSyncConstruct extends Construct { public readonly syncLambda: lambdaNodejs.NodejsFunction; constructor(scope: Construct, id: string, props: PoSyncProps) { super(scope, id); // Import the purchase-orders DynamoDB table (owned by po-ingest stack) const poTable = dynamodb.Table.fromTableName( this, 'PurchaseOrdersTable', 'purchase-orders', ); // Lambda — scans purchase-orders table, uploads markdown to S3, triggers KB ingestion this.syncLambda = new lambdaNodejs.NodejsFunction(this, 'SyncLambda', { functionName: 'seahaven-po-sync', entry: path.join(__dirname, '../../lambda/po-sync/index.ts'), runtime: lambda.Runtime.NODEJS_24_X, architecture: lambda.Architecture.ARM_64, logRetention: logs.RetentionDays.TWO_MONTHS, memorySize: 512, timeout: cdk.Duration.minutes(15), environment: { PO_TABLE: poTable.tableName, KB_BUCKET_NAME: props.kbDocsBucket.bucketName, KNOWLEDGE_BASE_ID: props.knowledgeBaseId, DATA_SOURCE_ID: props.dataSourceId, REGION: props.region, }, }); // Grant read access to the purchase-orders table poTable.grantReadData(this.syncLambda); // purchase-orders is SSE-encrypted with the shared customer-managed CMK // (INFRA-95 / M-3). The table is imported by name, so grantReadData does not // add KMS perms — grant kms:Decrypt explicitly or scans fail with // AccessDenied. (CMK key policy delegates to IAM via kms:ViaService.) const dynamodbCmk = kms.Key.fromKeyArn( this, 'DynamoDbCmk', ssm.StringParameter.valueForStringParameter(this, '/seahaven/dynamodb/cmk-arn'), ); dynamodbCmk.grantDecrypt(this.syncLambda); // Grant read/write to the KB docs bucket (read to list+delete old, write new) props.kbDocsBucket.grantReadWrite(this.syncLambda); // Allow Lambda to start a Bedrock KB ingestion job this.syncLambda.addToRolePolicy( new iam.PolicyStatement({ actions: ['bedrock:StartIngestionJob'], resources: [ `arn:aws:bedrock:${props.region}:*:knowledge-base/${props.knowledgeBaseId}`, ], }), ); // EventBridge rule — fires daily at 02:00 UTC const dailyRule = new events.Rule(this, 'DailySyncRule', { ruleName: 'seahaven-po-daily-sync', description: 'Daily purchase-orders → KB sync at 02:00 UTC', schedule: events.Schedule.cron({ minute: '0', hour: '2' }), }); dailyRule.addTarget(new targets.LambdaFunction(this.syncLambda)); } }