@mettlecast/domain-cdk-packer 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/dist/DomainStack.d.ts +68 -0
- package/dist/DomainStack.js +567 -0
- package/dist/__tests__/domain-stack.test.d.ts +1 -0
- package/dist/__tests__/domain-stack.test.js +223 -0
- package/dist/__tests__/lambda-factory.test.d.ts +1 -0
- package/dist/__tests__/lambda-factory.test.js +76 -0
- package/dist/__tests__/pack-flows.test.d.ts +1 -0
- package/dist/__tests__/pack-flows.test.js +283 -0
- package/dist/__tests__/registry.test.d.ts +1 -0
- package/dist/__tests__/registry.test.js +34 -0
- package/dist/__tests__/step-functions-codegen.test.d.ts +1 -0
- package/dist/__tests__/step-functions-codegen.test.js +159 -0
- package/dist/aspects/iam-boundaries-aspect.d.ts +11 -0
- package/dist/aspects/iam-boundaries-aspect.js +15 -0
- package/dist/aspects/index.d.ts +5 -0
- package/dist/aspects/index.js +3 -0
- package/dist/aspects/log-retention-aspect.d.ts +8 -0
- package/dist/aspects/log-retention-aspect.js +24 -0
- package/dist/aspects/tagging-aspect.d.ts +13 -0
- package/dist/aspects/tagging-aspect.js +17 -0
- package/dist/constructs/action-construct.d.ts +35 -0
- package/dist/constructs/action-construct.js +35 -0
- package/dist/constructs/alarm-construct.d.ts +19 -0
- package/dist/constructs/alarm-construct.js +58 -0
- package/dist/constructs/alarms-construct.d.ts +17 -0
- package/dist/constructs/alarms-construct.js +42 -0
- package/dist/constructs/api-construct.d.ts +30 -0
- package/dist/constructs/api-construct.js +64 -0
- package/dist/constructs/canary-construct.d.ts +13 -0
- package/dist/constructs/canary-construct.js +30 -0
- package/dist/constructs/cmk-construct.d.ts +9 -0
- package/dist/constructs/cmk-construct.js +20 -0
- package/dist/constructs/dashboard-construct.d.ts +15 -0
- package/dist/constructs/dashboard-construct.js +62 -0
- package/dist/constructs/health-construct.d.ts +11 -0
- package/dist/constructs/health-construct.js +29 -0
- package/dist/constructs/job-construct.d.ts +33 -0
- package/dist/constructs/job-construct.js +54 -0
- package/dist/constructs/schedule-construct.d.ts +27 -0
- package/dist/constructs/schedule-construct.js +54 -0
- package/dist/constructs/subscriber-construct.d.ts +30 -0
- package/dist/constructs/subscriber-construct.js +63 -0
- package/dist/constructs/waf-construct.d.ts +13 -0
- package/dist/constructs/waf-construct.js +75 -0
- package/dist/constructs/webhook-construct.d.ts +33 -0
- package/dist/constructs/webhook-construct.js +50 -0
- package/dist/flow-registry.d.ts +81 -0
- package/dist/flow-registry.js +2 -0
- package/dist/grouped-lambda-factory.d.ts +43 -0
- package/dist/grouped-lambda-factory.js +71 -0
- package/dist/iam/iam-policy-builder.d.ts +95 -0
- package/dist/iam/iam-policy-builder.js +232 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +17 -0
- package/dist/lambda-factory.d.ts +46 -0
- package/dist/lambda-factory.js +80 -0
- package/dist/pack-domain.d.ts +35 -0
- package/dist/pack-domain.js +32 -0
- package/dist/pack-flows.d.ts +28 -0
- package/dist/pack-flows.js +154 -0
- package/dist/registry.d.ts +224 -0
- package/dist/registry.js +1 -0
- package/dist/step-functions-codegen.d.ts +65 -0
- package/dist/step-functions-codegen.js +55 -0
- package/package.json +36 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
export interface DashboardConstructProps {
|
|
4
|
+
domainId: string;
|
|
5
|
+
envCode: string;
|
|
6
|
+
/** List of endpointIds per primitiveClass for dashboard panels. */
|
|
7
|
+
endpoints: Array<{
|
|
8
|
+
id: string;
|
|
9
|
+
primitiveClass: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export declare class DashboardConstruct extends Construct {
|
|
13
|
+
readonly dashboard: cloudwatch.Dashboard;
|
|
14
|
+
constructor(scope: Construct, id: string, props: DashboardConstructProps);
|
|
15
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
|
|
3
|
+
import { Construct } from 'constructs';
|
|
4
|
+
export class DashboardConstruct extends Construct {
|
|
5
|
+
dashboard;
|
|
6
|
+
constructor(scope, id, props) {
|
|
7
|
+
super(scope, id);
|
|
8
|
+
const { domainId, envCode, endpoints } = props;
|
|
9
|
+
const namespace = `TIB/Domain/${domainId}`;
|
|
10
|
+
const widgets = [];
|
|
11
|
+
// Group endpoints by primitiveClass for rows
|
|
12
|
+
const byClass = endpoints.reduce((acc, ep) => {
|
|
13
|
+
(acc[ep.primitiveClass] ??= []).push(ep.id);
|
|
14
|
+
return acc;
|
|
15
|
+
}, {});
|
|
16
|
+
for (const [primitiveClass, endpointIds] of Object.entries(byClass)) {
|
|
17
|
+
// Row title
|
|
18
|
+
widgets.push(new cloudwatch.TextWidget({
|
|
19
|
+
markdown: `## ${primitiveClass.toUpperCase()} — ${domainId}`,
|
|
20
|
+
width: 24,
|
|
21
|
+
height: 1,
|
|
22
|
+
}));
|
|
23
|
+
for (const endpointId of endpointIds) {
|
|
24
|
+
const dims = { domainId, endpointId, primitiveClass };
|
|
25
|
+
widgets.push(new cloudwatch.GraphWidget({
|
|
26
|
+
title: `${endpointId} — latency`,
|
|
27
|
+
width: 6,
|
|
28
|
+
height: 6,
|
|
29
|
+
left: [
|
|
30
|
+
new cloudwatch.Metric({ namespace, metricName: 'latency_ms', dimensionsMap: dims, statistic: 'p50', label: 'p50' }),
|
|
31
|
+
new cloudwatch.Metric({ namespace, metricName: 'latency_ms', dimensionsMap: dims, statistic: 'p95', label: 'p95' }),
|
|
32
|
+
new cloudwatch.Metric({ namespace, metricName: 'latency_ms', dimensionsMap: dims, statistic: 'p99', label: 'p99' }),
|
|
33
|
+
],
|
|
34
|
+
}), new cloudwatch.GraphWidget({
|
|
35
|
+
title: `${endpointId} — errors & cold starts`,
|
|
36
|
+
width: 6,
|
|
37
|
+
height: 6,
|
|
38
|
+
left: [
|
|
39
|
+
new cloudwatch.Metric({ namespace, metricName: 'errors', dimensionsMap: dims, statistic: 'Sum', label: 'errors' }),
|
|
40
|
+
new cloudwatch.Metric({ namespace, metricName: 'cold_starts', dimensionsMap: dims, statistic: 'Sum', label: 'cold_starts' }),
|
|
41
|
+
],
|
|
42
|
+
}), new cloudwatch.GraphWidget({
|
|
43
|
+
title: `${endpointId} — DB & events`,
|
|
44
|
+
width: 6,
|
|
45
|
+
height: 6,
|
|
46
|
+
left: [
|
|
47
|
+
new cloudwatch.Metric({ namespace, metricName: 'db_query_ms', dimensionsMap: dims, statistic: 'Average', label: 'db_ms_avg' }),
|
|
48
|
+
new cloudwatch.Metric({ namespace, metricName: 'event_publish_count', dimensionsMap: dims, statistic: 'Sum', label: 'events' }),
|
|
49
|
+
],
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this.dashboard = new cloudwatch.Dashboard(this, 'Dashboard', {
|
|
54
|
+
dashboardName: `TIB-${envCode}-domain-${domainId}`,
|
|
55
|
+
widgets: [widgets],
|
|
56
|
+
});
|
|
57
|
+
new cdk.CfnOutput(scope, 'DashboardArn', {
|
|
58
|
+
value: this.dashboard.dashboardArn,
|
|
59
|
+
exportName: `TIB-${envCode}-domain-${domainId}-DashboardArn`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
export interface HealthConstructProps {
|
|
4
|
+
domainId: string;
|
|
5
|
+
environment: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export declare class HealthConstruct extends Construct {
|
|
8
|
+
readonly healthFn: nodejs.NodejsFunction;
|
|
9
|
+
readonly readyFn: nodejs.NodejsFunction;
|
|
10
|
+
constructor(scope: Construct, id: string, props: HealthConstructProps);
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
3
|
+
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
4
|
+
import { Construct } from 'constructs';
|
|
5
|
+
export class HealthConstruct extends Construct {
|
|
6
|
+
healthFn;
|
|
7
|
+
readyFn;
|
|
8
|
+
constructor(scope, id, props) {
|
|
9
|
+
super(scope, id);
|
|
10
|
+
const commonProps = {
|
|
11
|
+
runtime: lambda.Runtime.NODEJS_20_X,
|
|
12
|
+
environment: props.environment,
|
|
13
|
+
timeout: cdk.Duration.seconds(10),
|
|
14
|
+
memorySize: 128,
|
|
15
|
+
};
|
|
16
|
+
this.healthFn = new nodejs.NodejsFunction(this, 'HealthFn', {
|
|
17
|
+
...commonProps,
|
|
18
|
+
entry: require.resolve('@mettlecast/domain-runtime/runtime/health-handler'),
|
|
19
|
+
handler: 'healthHandler',
|
|
20
|
+
description: `${props.domainId} /_health — Lambda warm check`,
|
|
21
|
+
});
|
|
22
|
+
this.readyFn = new nodejs.NodejsFunction(this, 'ReadyFn', {
|
|
23
|
+
...commonProps,
|
|
24
|
+
entry: require.resolve('@mettlecast/domain-runtime/runtime/health-handler'),
|
|
25
|
+
handler: 'readinessHandler',
|
|
26
|
+
description: `${props.domainId} /_ready — dependency reachability check`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as sqs from 'aws-cdk-lib/aws-sqs';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { LambdaFactory } from '../lambda-factory.js';
|
|
4
|
+
import { IamPolicyBuilder } from '../iam/iam-policy-builder.js';
|
|
5
|
+
import type { DomainRegistry } from '../registry.js';
|
|
6
|
+
/**
|
|
7
|
+
* Configuration properties for JobConstruct.
|
|
8
|
+
*/
|
|
9
|
+
export interface JobConstructProps {
|
|
10
|
+
/** Domain registry containing job definitions. */
|
|
11
|
+
registry: DomainRegistry;
|
|
12
|
+
/** Factory for creating Lambda functions. */
|
|
13
|
+
lambdaFactory: LambdaFactory;
|
|
14
|
+
/** Builder for generating IAM policies. */
|
|
15
|
+
iamBuilder: IamPolicyBuilder;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* CDK Construct that synthesizes Lambda functions with SQS queues, dead-letter queues,
|
|
19
|
+
* and event source mappings for background jobs defined in the domain registry.
|
|
20
|
+
*/
|
|
21
|
+
export declare class JobConstruct extends Construct {
|
|
22
|
+
/**
|
|
23
|
+
* Map of job IDs to their corresponding SQS queues.
|
|
24
|
+
*/
|
|
25
|
+
readonly queues: Map<string, sqs.Queue>;
|
|
26
|
+
/**
|
|
27
|
+
* Create a new JobConstruct.
|
|
28
|
+
* @param scope - Parent CDK scope
|
|
29
|
+
* @param id - Construct identifier
|
|
30
|
+
* @param props - Configuration properties
|
|
31
|
+
*/
|
|
32
|
+
constructor(scope: Construct, id: string, props: JobConstructProps);
|
|
33
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as sqs from 'aws-cdk-lib/aws-sqs';
|
|
3
|
+
import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources';
|
|
4
|
+
import { Construct } from 'constructs';
|
|
5
|
+
/**
|
|
6
|
+
* CDK Construct that synthesizes Lambda functions with SQS queues, dead-letter queues,
|
|
7
|
+
* and event source mappings for background jobs defined in the domain registry.
|
|
8
|
+
*/
|
|
9
|
+
export class JobConstruct extends Construct {
|
|
10
|
+
/**
|
|
11
|
+
* Map of job IDs to their corresponding SQS queues.
|
|
12
|
+
*/
|
|
13
|
+
queues;
|
|
14
|
+
/**
|
|
15
|
+
* Create a new JobConstruct.
|
|
16
|
+
* @param scope - Parent CDK scope
|
|
17
|
+
* @param id - Construct identifier
|
|
18
|
+
* @param props - Configuration properties
|
|
19
|
+
*/
|
|
20
|
+
constructor(scope, id, props) {
|
|
21
|
+
super(scope, id);
|
|
22
|
+
this.queues = new Map();
|
|
23
|
+
for (const entry of props.registry.jobs) {
|
|
24
|
+
const pascalId = toPascalCase(entry.id);
|
|
25
|
+
const dlq = new sqs.Queue(this, `${pascalId}Dlq`, {
|
|
26
|
+
retentionPeriod: cdk.Duration.days(14),
|
|
27
|
+
});
|
|
28
|
+
const queue = new sqs.Queue(this, `${pascalId}Queue`, {
|
|
29
|
+
visibilityTimeout: cdk.Duration.seconds(entry.visibilityTimeoutSeconds),
|
|
30
|
+
deadLetterQueue: {
|
|
31
|
+
queue: dlq,
|
|
32
|
+
maxReceiveCount: entry.maxRetries,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
this.queues.set(entry.id, queue);
|
|
36
|
+
const fn = props.lambdaFactory.createFunction(entry, entry.deployment);
|
|
37
|
+
props.iamBuilder
|
|
38
|
+
.forJob({ queueArn: queue.queueArn, dlqArn: dlq.queueArn })
|
|
39
|
+
.forEach((s) => fn.addToRolePolicy(s));
|
|
40
|
+
fn.addEventSource(new lambdaEventSources.SqsEventSource(queue, { batchSize: 1 }));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Convert a kebab-case or snake_case string to PascalCase.
|
|
46
|
+
* @param s - Input string to convert
|
|
47
|
+
* @returns PascalCase string
|
|
48
|
+
*/
|
|
49
|
+
function toPascalCase(s) {
|
|
50
|
+
return s
|
|
51
|
+
.split(/[-_]/)
|
|
52
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
53
|
+
.join('');
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Construct } from 'constructs';
|
|
2
|
+
import { LambdaFactory } from '../lambda-factory.js';
|
|
3
|
+
import { IamPolicyBuilder } from '../iam/iam-policy-builder.js';
|
|
4
|
+
import type { DomainRegistry } from '../registry.js';
|
|
5
|
+
/**
|
|
6
|
+
* Configuration properties for ScheduleConstruct.
|
|
7
|
+
*/
|
|
8
|
+
export interface ScheduleConstructProps {
|
|
9
|
+
/** Domain registry containing schedule definitions */
|
|
10
|
+
registry: DomainRegistry;
|
|
11
|
+
/** Lambda factory for creating schedule handler functions */
|
|
12
|
+
lambdaFactory: LambdaFactory;
|
|
13
|
+
/** IAM policy builder for schedule handler permissions */
|
|
14
|
+
iamBuilder: IamPolicyBuilder;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* CDK Construct that synthesises Lambda functions and EventBridge Scheduler rules per schedule entry.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ScheduleConstruct extends Construct {
|
|
20
|
+
/**
|
|
21
|
+
* Create a new ScheduleConstruct.
|
|
22
|
+
* @param scope - Parent CDK Construct scope
|
|
23
|
+
* @param id - Logical ID for this construct
|
|
24
|
+
* @param props - Configuration properties
|
|
25
|
+
*/
|
|
26
|
+
constructor(scope: Construct, id: string, props: ScheduleConstructProps);
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
2
|
+
import * as scheduler from 'aws-cdk-lib/aws-scheduler';
|
|
3
|
+
import { Construct } from 'constructs';
|
|
4
|
+
/**
|
|
5
|
+
* CDK Construct that synthesises Lambda functions and EventBridge Scheduler rules per schedule entry.
|
|
6
|
+
*/
|
|
7
|
+
export class ScheduleConstruct extends Construct {
|
|
8
|
+
/**
|
|
9
|
+
* Create a new ScheduleConstruct.
|
|
10
|
+
* @param scope - Parent CDK Construct scope
|
|
11
|
+
* @param id - Logical ID for this construct
|
|
12
|
+
* @param props - Configuration properties
|
|
13
|
+
*/
|
|
14
|
+
constructor(scope, id, props) {
|
|
15
|
+
super(scope, id);
|
|
16
|
+
for (const entry of props.registry.schedules) {
|
|
17
|
+
const pascalId = toPascalCase(entry.id);
|
|
18
|
+
// Create the Lambda function for this schedule
|
|
19
|
+
const fn = props.lambdaFactory.createFunction(entry, entry.deployment);
|
|
20
|
+
// Attach schedule-specific IAM policies
|
|
21
|
+
props.iamBuilder.forSchedule().forEach((statement) => {
|
|
22
|
+
fn.addToRolePolicy(statement);
|
|
23
|
+
});
|
|
24
|
+
// Create a dedicated IAM role for the EventBridge Scheduler to assume
|
|
25
|
+
const schedulerRole = new iam.Role(this, `${pascalId}SchedulerRole`, {
|
|
26
|
+
assumedBy: new iam.ServicePrincipal('scheduler.amazonaws.com'),
|
|
27
|
+
});
|
|
28
|
+
// Grant the scheduler role permission to invoke the Lambda
|
|
29
|
+
fn.grantInvoke(schedulerRole);
|
|
30
|
+
// Create the EventBridge Scheduler rule
|
|
31
|
+
new scheduler.CfnSchedule(this, `${pascalId}Schedule`, {
|
|
32
|
+
scheduleExpression: entry.cron,
|
|
33
|
+
flexibleTimeWindow: { mode: 'OFF' },
|
|
34
|
+
state: entry.enabled ? 'ENABLED' : 'DISABLED',
|
|
35
|
+
target: {
|
|
36
|
+
arn: fn.functionArn,
|
|
37
|
+
roleArn: schedulerRole.roleArn,
|
|
38
|
+
input: JSON.stringify({}),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Convert a kebab-case or snake_case string to PascalCase.
|
|
46
|
+
* @param s - Input string
|
|
47
|
+
* @returns PascalCase string
|
|
48
|
+
*/
|
|
49
|
+
function toPascalCase(s) {
|
|
50
|
+
return s
|
|
51
|
+
.split(/[-_]/)
|
|
52
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
53
|
+
.join('');
|
|
54
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as events from 'aws-cdk-lib/aws-events';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { LambdaFactory } from '../lambda-factory.js';
|
|
4
|
+
import { IamPolicyBuilder } from '../iam/iam-policy-builder.js';
|
|
5
|
+
import type { DomainRegistry } from '../registry.js';
|
|
6
|
+
/**
|
|
7
|
+
* Configuration properties for SubscriberConstruct.
|
|
8
|
+
*/
|
|
9
|
+
export interface SubscriberConstructProps {
|
|
10
|
+
/** Domain registry containing all subscriber entries. */
|
|
11
|
+
registry: DomainRegistry;
|
|
12
|
+
/** Factory for creating Lambda functions from registry entries. */
|
|
13
|
+
lambdaFactory: LambdaFactory;
|
|
14
|
+
/** IAM policy builder for granting permissions to Lambda roles. */
|
|
15
|
+
iamBuilder: IamPolicyBuilder;
|
|
16
|
+
/** EventBridge event bus for routing subscriber events. */
|
|
17
|
+
eventBus: events.IEventBus;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* CDK Construct that synthesises Lambda + SQS + EventBridge rule per registered subscriber.
|
|
21
|
+
*/
|
|
22
|
+
export declare class SubscriberConstruct extends Construct {
|
|
23
|
+
/**
|
|
24
|
+
* Create a new SubscriberConstruct instance.
|
|
25
|
+
* @param scope - Parent CDK scope.
|
|
26
|
+
* @param id - Construct identifier.
|
|
27
|
+
* @param props - Construct properties.
|
|
28
|
+
*/
|
|
29
|
+
constructor(scope: Construct, id: string, props: SubscriberConstructProps);
|
|
30
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as sqs from 'aws-cdk-lib/aws-sqs';
|
|
3
|
+
import * as events from 'aws-cdk-lib/aws-events';
|
|
4
|
+
import * as eventsTargets from 'aws-cdk-lib/aws-events-targets';
|
|
5
|
+
import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources';
|
|
6
|
+
import { Construct } from 'constructs';
|
|
7
|
+
/**
|
|
8
|
+
* CDK Construct that synthesises Lambda + SQS + EventBridge rule per registered subscriber.
|
|
9
|
+
*/
|
|
10
|
+
export class SubscriberConstruct extends Construct {
|
|
11
|
+
/**
|
|
12
|
+
* Create a new SubscriberConstruct instance.
|
|
13
|
+
* @param scope - Parent CDK scope.
|
|
14
|
+
* @param id - Construct identifier.
|
|
15
|
+
* @param props - Construct properties.
|
|
16
|
+
*/
|
|
17
|
+
constructor(scope, id, props) {
|
|
18
|
+
super(scope, id);
|
|
19
|
+
for (const entry of props.registry.subscribers) {
|
|
20
|
+
const pascalId = toPascalCase(entry.id);
|
|
21
|
+
// Create dead-letter queue
|
|
22
|
+
const dlq = new sqs.Queue(this, `${pascalId}Dlq`, {
|
|
23
|
+
retentionPeriod: cdk.Duration.days(14),
|
|
24
|
+
});
|
|
25
|
+
// Create main queue with dead-letter queue configuration
|
|
26
|
+
const queue = new sqs.Queue(this, `${pascalId}Queue`, {
|
|
27
|
+
visibilityTimeout: cdk.Duration.seconds(180),
|
|
28
|
+
deadLetterQueue: {
|
|
29
|
+
queue: dlq,
|
|
30
|
+
maxReceiveCount: 3,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
// Create Lambda function from registry entry
|
|
34
|
+
const fn = props.lambdaFactory.createFunction(entry, entry.deployment);
|
|
35
|
+
// Add IAM policies for SQS access
|
|
36
|
+
props.iamBuilder.forSubscriber({ queueArn: queue.queueArn, dlqArn: dlq.queueArn }).forEach((statement) => {
|
|
37
|
+
fn.addToRolePolicy(statement);
|
|
38
|
+
});
|
|
39
|
+
// Add SQS as event source for Lambda
|
|
40
|
+
fn.addEventSource(new lambdaEventSources.SqsEventSource(queue, {
|
|
41
|
+
batchSize: 10,
|
|
42
|
+
maxConcurrency: entry.concurrency ?? 5,
|
|
43
|
+
}));
|
|
44
|
+
// Create EventBridge rule targeting SQS queue
|
|
45
|
+
new events.Rule(this, `${pascalId}Rule`, {
|
|
46
|
+
eventBus: props.eventBus,
|
|
47
|
+
eventPattern: { detailType: [entry.event] },
|
|
48
|
+
targets: [new eventsTargets.SqsQueue(queue)],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Convert kebab-case or snake_case string to PascalCase.
|
|
55
|
+
* @param s - Input string.
|
|
56
|
+
* @returns PascalCase string.
|
|
57
|
+
*/
|
|
58
|
+
function toPascalCase(s) {
|
|
59
|
+
return s
|
|
60
|
+
.split(/[-_]/)
|
|
61
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
62
|
+
.join('');
|
|
63
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
|
|
2
|
+
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
|
|
3
|
+
import { Construct } from 'constructs';
|
|
4
|
+
export interface WafConstructProps {
|
|
5
|
+
domainId: string;
|
|
6
|
+
httpApi: apigwv2.HttpApi;
|
|
7
|
+
/** Requests per 5-minute window per IP before blocking. Default: 2000. */
|
|
8
|
+
rateLimit?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class WafConstruct extends Construct {
|
|
11
|
+
readonly webAcl: wafv2.CfnWebACL;
|
|
12
|
+
constructor(scope: Construct, id: string, props: WafConstructProps);
|
|
13
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
|
|
3
|
+
import { Construct } from 'constructs';
|
|
4
|
+
export class WafConstruct extends Construct {
|
|
5
|
+
webAcl;
|
|
6
|
+
constructor(scope, id, props) {
|
|
7
|
+
super(scope, id);
|
|
8
|
+
const rateLimit = props.rateLimit ?? 2000;
|
|
9
|
+
this.webAcl = new wafv2.CfnWebACL(this, 'WebAcl', {
|
|
10
|
+
name: `tib-${props.domainId}-waf`,
|
|
11
|
+
scope: 'REGIONAL',
|
|
12
|
+
defaultAction: { allow: {} },
|
|
13
|
+
visibilityConfig: {
|
|
14
|
+
sampledRequestsEnabled: true,
|
|
15
|
+
cloudWatchMetricsEnabled: true,
|
|
16
|
+
metricName: `tib-${props.domainId}-waf`,
|
|
17
|
+
},
|
|
18
|
+
rules: [
|
|
19
|
+
{
|
|
20
|
+
name: 'AWSManagedRulesCommonRuleSet',
|
|
21
|
+
priority: 1,
|
|
22
|
+
overrideAction: { none: {} },
|
|
23
|
+
statement: {
|
|
24
|
+
managedRuleGroupStatement: {
|
|
25
|
+
vendorName: 'AWS',
|
|
26
|
+
name: 'AWSManagedRulesCommonRuleSet',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
visibilityConfig: {
|
|
30
|
+
sampledRequestsEnabled: true,
|
|
31
|
+
cloudWatchMetricsEnabled: true,
|
|
32
|
+
metricName: `tib-${props.domainId}-common-rules`,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'AWSManagedRulesKnownBadInputsRuleSet',
|
|
37
|
+
priority: 2,
|
|
38
|
+
overrideAction: { none: {} },
|
|
39
|
+
statement: {
|
|
40
|
+
managedRuleGroupStatement: {
|
|
41
|
+
vendorName: 'AWS',
|
|
42
|
+
name: 'AWSManagedRulesKnownBadInputsRuleSet',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
visibilityConfig: {
|
|
46
|
+
sampledRequestsEnabled: true,
|
|
47
|
+
cloudWatchMetricsEnabled: true,
|
|
48
|
+
metricName: `tib-${props.domainId}-bad-inputs`,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'IpRateLimit',
|
|
53
|
+
priority: 3,
|
|
54
|
+
action: { block: {} },
|
|
55
|
+
statement: {
|
|
56
|
+
rateBasedStatement: {
|
|
57
|
+
limit: rateLimit,
|
|
58
|
+
aggregateKeyType: 'IP',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
visibilityConfig: {
|
|
62
|
+
sampledRequestsEnabled: true,
|
|
63
|
+
cloudWatchMetricsEnabled: true,
|
|
64
|
+
metricName: `tib-${props.domainId}-rate-limit`,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
// Associate WAF with the API Gateway stage
|
|
70
|
+
new wafv2.CfnWebACLAssociation(this, 'WebAclAssociation', {
|
|
71
|
+
resourceArn: `arn:aws:apigateway:${cdk.Stack.of(this).region}::/restapis/${props.httpApi.httpApiId}/stages/$default`,
|
|
72
|
+
webAclArn: this.webAcl.attrArn,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
|
|
2
|
+
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
|
|
3
|
+
import { Construct } from 'constructs';
|
|
4
|
+
import { LambdaFactory } from '../lambda-factory.js';
|
|
5
|
+
import { IamPolicyBuilder } from '../iam/iam-policy-builder.js';
|
|
6
|
+
import type { DomainRegistry } from '../registry.js';
|
|
7
|
+
/**
|
|
8
|
+
* Configuration properties for WebhookConstruct.
|
|
9
|
+
*/
|
|
10
|
+
export interface WebhookConstructProps {
|
|
11
|
+
/** Domain registry containing all webhook entries. */
|
|
12
|
+
registry: DomainRegistry;
|
|
13
|
+
/** Shared HttpApi to route requests to Lambdas. */
|
|
14
|
+
httpApi: apigwv2.HttpApi;
|
|
15
|
+
/** Factory for creating Lambda functions from registry entries. */
|
|
16
|
+
lambdaFactory: LambdaFactory;
|
|
17
|
+
/** IAM policy builder for granting permissions to Lambda roles. */
|
|
18
|
+
iamBuilder: IamPolicyBuilder;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* CDK Construct that synthesises one Lambda per registered webhook, one shared DynamoDB dedupe table, and adds webhook routes to the HttpApi.
|
|
22
|
+
*/
|
|
23
|
+
export declare class WebhookConstruct extends Construct {
|
|
24
|
+
/** Shared webhook deduplication table. */
|
|
25
|
+
readonly dedupeTable: dynamodb.Table;
|
|
26
|
+
/**
|
|
27
|
+
* Create a new WebhookConstruct instance.
|
|
28
|
+
* @param scope - Parent CDK scope.
|
|
29
|
+
* @param id - Construct identifier.
|
|
30
|
+
* @param props - Construct properties.
|
|
31
|
+
*/
|
|
32
|
+
constructor(scope: Construct, id: string, props: WebhookConstructProps);
|
|
33
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
|
|
3
|
+
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
|
|
4
|
+
import * as apigwv2integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
|
|
5
|
+
import { Construct } from 'constructs';
|
|
6
|
+
/**
|
|
7
|
+
* CDK Construct that synthesises one Lambda per registered webhook, one shared DynamoDB dedupe table, and adds webhook routes to the HttpApi.
|
|
8
|
+
*/
|
|
9
|
+
export class WebhookConstruct extends Construct {
|
|
10
|
+
/** Shared webhook deduplication table. */
|
|
11
|
+
dedupeTable;
|
|
12
|
+
/**
|
|
13
|
+
* Create a new WebhookConstruct instance.
|
|
14
|
+
* @param scope - Parent CDK scope.
|
|
15
|
+
* @param id - Construct identifier.
|
|
16
|
+
* @param props - Construct properties.
|
|
17
|
+
*/
|
|
18
|
+
constructor(scope, id, props) {
|
|
19
|
+
super(scope, id);
|
|
20
|
+
this.dedupeTable = new dynamodb.Table(this, 'DedupeTable', {
|
|
21
|
+
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
|
|
22
|
+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
23
|
+
timeToLiveAttribute: 'expiresAt',
|
|
24
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
25
|
+
});
|
|
26
|
+
for (const entry of props.registry.webhooks) {
|
|
27
|
+
const fn = props.lambdaFactory.createFunction(entry, entry.deployment);
|
|
28
|
+
props.iamBuilder.forWebhook({ dedupeTableArn: this.dedupeTable.tableArn }).forEach((statement) => {
|
|
29
|
+
fn.addToRolePolicy(statement);
|
|
30
|
+
});
|
|
31
|
+
this.dedupeTable.grantReadWriteData(fn);
|
|
32
|
+
props.httpApi.addRoutes({
|
|
33
|
+
path: entry.path,
|
|
34
|
+
methods: [apigwv2.HttpMethod.POST],
|
|
35
|
+
integration: new apigwv2integrations.HttpLambdaIntegration(`${id}${toPascalCase(entry.id)}Integration`, fn),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Convert kebab-case or snake_case string to PascalCase.
|
|
42
|
+
* @param s - Input string.
|
|
43
|
+
* @returns PascalCase string.
|
|
44
|
+
*/
|
|
45
|
+
function toPascalCase(s) {
|
|
46
|
+
return s
|
|
47
|
+
.split(/[-_]/)
|
|
48
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
49
|
+
.join('');
|
|
50
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Plain JSON-serialisable mirror of FlowStep types from domain-runtime. */
|
|
2
|
+
export interface SerialDomainActionStep {
|
|
3
|
+
type: 'domain-action';
|
|
4
|
+
name: string;
|
|
5
|
+
domainId: string;
|
|
6
|
+
actionId: string;
|
|
7
|
+
parameters?: Record<string, unknown>;
|
|
8
|
+
waitForTaskToken?: boolean;
|
|
9
|
+
next?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SerialDomainApiStep {
|
|
12
|
+
type: 'domain-api';
|
|
13
|
+
name: string;
|
|
14
|
+
domainId: string;
|
|
15
|
+
apiId: string;
|
|
16
|
+
parameters?: Record<string, unknown>;
|
|
17
|
+
next?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SerialDomainEventStep {
|
|
20
|
+
type: 'domain-event';
|
|
21
|
+
name: string;
|
|
22
|
+
eventId: string;
|
|
23
|
+
payload: Record<string, unknown>;
|
|
24
|
+
version: number;
|
|
25
|
+
next?: string;
|
|
26
|
+
}
|
|
27
|
+
export type SerialFlowControlStep = {
|
|
28
|
+
type: 'flow-control';
|
|
29
|
+
control: 'choice';
|
|
30
|
+
name: string;
|
|
31
|
+
choices: Array<{
|
|
32
|
+
when: string;
|
|
33
|
+
next: string;
|
|
34
|
+
}>;
|
|
35
|
+
default?: string;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'flow-control';
|
|
38
|
+
control: 'parallel';
|
|
39
|
+
name: string;
|
|
40
|
+
branches: SerialFlowStep[][];
|
|
41
|
+
next?: string;
|
|
42
|
+
} | {
|
|
43
|
+
type: 'flow-control';
|
|
44
|
+
control: 'wait';
|
|
45
|
+
name: string;
|
|
46
|
+
seconds: number;
|
|
47
|
+
next?: string;
|
|
48
|
+
} | {
|
|
49
|
+
type: 'flow-control';
|
|
50
|
+
control: 'succeed';
|
|
51
|
+
name: string;
|
|
52
|
+
} | {
|
|
53
|
+
type: 'flow-control';
|
|
54
|
+
control: 'fail';
|
|
55
|
+
name: string;
|
|
56
|
+
cause?: string;
|
|
57
|
+
error?: string;
|
|
58
|
+
} | {
|
|
59
|
+
type: 'flow-control';
|
|
60
|
+
control: 'pass';
|
|
61
|
+
name: string;
|
|
62
|
+
result?: unknown;
|
|
63
|
+
next?: string;
|
|
64
|
+
};
|
|
65
|
+
export type SerialFlowStep = SerialDomainActionStep | SerialDomainApiStep | SerialDomainEventStep | SerialFlowControlStep;
|
|
66
|
+
export interface FlowRegistryEntry {
|
|
67
|
+
id: string;
|
|
68
|
+
owningDomain?: string;
|
|
69
|
+
type?: 'express' | 'standard';
|
|
70
|
+
name: string;
|
|
71
|
+
steps: SerialFlowStep[];
|
|
72
|
+
trigger?: {
|
|
73
|
+
type: 'event';
|
|
74
|
+
eventId: string;
|
|
75
|
+
semverRange: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export interface FlowRegistry {
|
|
79
|
+
schemaVersion: '1';
|
|
80
|
+
flows: FlowRegistryEntry[];
|
|
81
|
+
}
|