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 apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2'; import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as route53 from 'aws-cdk-lib/aws-route53'; import * as route53Targets from 'aws-cdk-lib/aws-route53-targets'; import * as acm from 'aws-cdk-lib/aws-certificatemanager'; import * as path from 'path'; export interface SlackHandlerProps { accountId: string; region: string; agentId: string; agentAliasId: string; conversationTable: dynamodb.Table; wildcardCertArn: string; } export class SlackHandlerConstruct extends Construct { public readonly webhookLambda: lambdaNodejs.NodejsFunction; public readonly processorLambda: lambdaNodejs.NodejsFunction; public readonly api: apigatewayv2.HttpApi; constructor(scope: Construct, id: string, props: SlackHandlerProps) { super(scope, id); // Slack credentials secret (created manually — see README for structure) const slackSecret = secretsmanager.Secret.fromSecretNameV2( this, 'SlackSecret', 'seahaven/slack/credentials', ); // ── Processor Lambda ────────────────────────────────────────────────────── // Async worker: calls Bedrock Agent, writes to DynamoDB, posts reply to Slack. // Timeout is generous — agent invocations with multi-step tool use can take 2–3 min. this.processorLambda = new lambdaNodejs.NodejsFunction(this, 'ProcessorFn', { functionName: 'seahaven-slack-processor', entry: path.join(__dirname, '../../lambda/slack-processor/index.ts'), handler: 'handler', runtime: lambda.Runtime.NODEJS_22_X, timeout: cdk.Duration.minutes(5), memorySize: 512, environment: { AGENT_ID: props.agentId, AGENT_ALIAS_ID: props.agentAliasId, CONVERSATION_TABLE: props.conversationTable.tableName, SLACK_SECRET_ARN: slackSecret.secretArn, REGION: props.region, }, bundling: { externalModules: ['@aws-sdk/*'], minify: true, sourceMap: false, }, }); slackSecret.grantRead(this.processorLambda); props.conversationTable.grantReadWriteData(this.processorLambda); this.processorLambda.addToRolePolicy(new iam.PolicyStatement({ actions: ['bedrock:InvokeAgent'], resources: [ // Wildcard on agent alias — agentAliasId is a CDK token resolved at synth `arn:aws:bedrock:${props.region}:${props.accountId}:agent/${props.agentId}`, `arn:aws:bedrock:${props.region}:${props.accountId}:agent-alias/${props.agentId}/*`, ], })); // ── Webhook Lambda ──────────────────────────────────────────────────────── // Synchronous: verifies Slack signature, returns 200 immediately, fires processor async. this.webhookLambda = new lambdaNodejs.NodejsFunction(this, 'WebhookFn', { functionName: 'seahaven-slack-webhook', entry: path.join(__dirname, '../../lambda/slack-webhook/index.ts'), handler: 'handler', runtime: lambda.Runtime.NODEJS_22_X, timeout: cdk.Duration.seconds(10), memorySize: 256, environment: { PROCESSOR_FUNCTION_NAME: this.processorLambda.functionName, SLACK_SECRET_ARN: slackSecret.secretArn, }, bundling: { externalModules: ['@aws-sdk/*'], minify: true, sourceMap: false, }, }); slackSecret.grantRead(this.webhookLambda); this.processorLambda.grantInvoke(this.webhookLambda); // ── HTTP API (API Gateway v2) ────────────────────────────────────────────── this.api = new apigatewayv2.HttpApi(this, 'Api', { apiName: 'seahaven-slack-webhook', description: 'Receives Slack event webhook calls for Sea Haven bot', }); this.api.addRoutes({ path: '/slack/events', methods: [apigatewayv2.HttpMethod.POST], integration: new HttpLambdaIntegration('WebhookIntegration', this.webhookLambda), }); // ── Custom domain: bot.seahaven.com ─────────────────────────────────────── const certificate = acm.Certificate.fromCertificateArn( this, 'WildcardCert', props.wildcardCertArn, ); const hostedZone = route53.HostedZone.fromLookup(this, 'SeahavenZone', { domainName: 'seahaven.com', }); const customDomain = new apigatewayv2.DomainName(this, 'CustomDomain', { domainName: 'bot.seahaven.com', certificate, }); new apigatewayv2.ApiMapping(this, 'ApiMapping', { api: this.api, domainName: customDomain, stage: this.api.defaultStage!, }); new route53.ARecord(this, 'BotDnsRecord', { zone: hostedZone, recordName: 'bot', target: route53.RecordTarget.fromAlias( new route53Targets.ApiGatewayv2DomainProperties( customDomain.regionalDomainName, customDomain.regionalHostedZoneId, ), ), }); new cdk.CfnOutput(scope, 'SlackWebhookUrl', { value: 'https://bot.seahaven.com/slack/events', description: 'Paste this into Slack app → Event Subscriptions → Request URL', }); } }