@maestro-js/aws-cdk-recipes 1.0.0-alpha.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/dist/index.js ADDED
@@ -0,0 +1,2252 @@
1
+ // src/bucket.ts
2
+ import { Construct } from "constructs";
3
+ import { Duration, RemovalPolicy } from "aws-cdk-lib";
4
+ import * as s3 from "aws-cdk-lib/aws-s3";
5
+ import * as iam from "aws-cdk-lib/aws-iam";
6
+ var Bucket2 = class extends Construct {
7
+ bucket;
8
+ constructor(scope, id, props = {}) {
9
+ super(scope, id);
10
+ const cors = resolveCorsRules(props.cors);
11
+ let bucketProps = {
12
+ blockPublicAccess: props.access === "public" ? resolvePublicAccessBlock() : s3.BlockPublicAccess.BLOCK_ALL,
13
+ objectOwnership: props.access === "public" ? s3.ObjectOwnership.BUCKET_OWNER_ENFORCED : void 0,
14
+ removalPolicy: RemovalPolicy.RETAIN,
15
+ versioned: props.versioning ?? false,
16
+ cors,
17
+ lifecycleRules: props.lifecycle ? resolveLifecycleRules(props.lifecycle) : void 0
18
+ };
19
+ if (props.transform?.bucket) {
20
+ bucketProps = props.transform.bucket(bucketProps);
21
+ }
22
+ this.bucket = new s3.Bucket(this, "Bucket", bucketProps);
23
+ if (props.enforceHttps !== false) {
24
+ this.bucket.addToResourcePolicy(
25
+ new iam.PolicyStatement({
26
+ effect: iam.Effect.DENY,
27
+ principals: [new iam.AnyPrincipal()],
28
+ actions: ["s3:*"],
29
+ resources: [this.bucket.bucketArn, `${this.bucket.bucketArn}/*`],
30
+ conditions: { Bool: { "aws:SecureTransport": "false" } }
31
+ })
32
+ );
33
+ }
34
+ if (props.access === "public") {
35
+ this.bucket.addToResourcePolicy(
36
+ new iam.PolicyStatement({
37
+ effect: iam.Effect.ALLOW,
38
+ principals: [new iam.AnyPrincipal()],
39
+ actions: ["s3:GetObject"],
40
+ resources: [`${this.bucket.bucketArn}/*`]
41
+ })
42
+ );
43
+ }
44
+ if (props.policy) {
45
+ for (const stmt of props.policy) {
46
+ this.bucket.addToResourcePolicy(resolvePolicyStatement(this.bucket, stmt));
47
+ }
48
+ }
49
+ }
50
+ };
51
+ function resolvePublicAccessBlock() {
52
+ return new s3.BlockPublicAccess({
53
+ blockPublicAcls: true,
54
+ ignorePublicAcls: true,
55
+ blockPublicPolicy: false,
56
+ restrictPublicBuckets: false
57
+ });
58
+ }
59
+ function resolveCorsRules(cors) {
60
+ if (cors === false) return void 0;
61
+ if (cors === void 0) {
62
+ return [
63
+ {
64
+ allowedHeaders: ["*"],
65
+ allowedMethods: [
66
+ s3.HttpMethods.GET,
67
+ s3.HttpMethods.PUT,
68
+ s3.HttpMethods.POST,
69
+ s3.HttpMethods.DELETE,
70
+ s3.HttpMethods.HEAD
71
+ ],
72
+ allowedOrigins: ["*"]
73
+ }
74
+ ];
75
+ }
76
+ return [
77
+ {
78
+ allowedHeaders: cors.allowHeaders,
79
+ allowedMethods: (cors.allowMethods ?? ["GET"]).map(resolveCorsMethods),
80
+ allowedOrigins: cors.allowOrigins ?? ["*"],
81
+ exposedHeaders: cors.exposeHeaders,
82
+ maxAge: cors.maxAge
83
+ }
84
+ ];
85
+ }
86
+ function resolveCorsMethods(method) {
87
+ switch (method) {
88
+ case "GET":
89
+ return s3.HttpMethods.GET;
90
+ case "PUT":
91
+ return s3.HttpMethods.PUT;
92
+ case "POST":
93
+ return s3.HttpMethods.POST;
94
+ case "DELETE":
95
+ return s3.HttpMethods.DELETE;
96
+ case "HEAD":
97
+ return s3.HttpMethods.HEAD;
98
+ }
99
+ }
100
+ function resolveLifecycleRules(rules) {
101
+ return rules.map((rule) => {
102
+ if (!rule.expiresIn && !rule.expiresAt) {
103
+ throw new Error('LifecycleRule requires either "expiresIn" or "expiresAt"');
104
+ }
105
+ const expirationDate = rule.expiresAt ? new Date(rule.expiresAt) : void 0;
106
+ if (expirationDate && isNaN(expirationDate.getTime())) {
107
+ throw new Error(`Invalid "expiresAt" date: "${rule.expiresAt}"`);
108
+ }
109
+ return {
110
+ id: rule.id,
111
+ prefix: rule.prefix,
112
+ enabled: rule.enabled ?? true,
113
+ expiration: rule.expiresIn ? parseDurationDays(rule.expiresIn) : void 0,
114
+ expirationDate
115
+ };
116
+ });
117
+ }
118
+ function parseDurationDays(duration) {
119
+ const match = duration.match(/^(\d+)\s*days?$/);
120
+ if (!match) throw new Error(`Invalid duration: "${duration}". Expected format: "30 days"`);
121
+ return Duration.days(Number(match[1]));
122
+ }
123
+ function resolvePolicyStatement(bucket, stmt) {
124
+ const principals = resolvePrincipals(stmt.principals);
125
+ const resources = stmt.paths ? stmt.paths.map((r) => `${bucket.bucketArn}/${r}`) : [bucket.bucketArn, `${bucket.bucketArn}/*`];
126
+ return new iam.PolicyStatement({
127
+ effect: stmt.effect === "deny" ? iam.Effect.DENY : iam.Effect.ALLOW,
128
+ principals,
129
+ actions: stmt.actions,
130
+ resources,
131
+ conditions: stmt.conditions ? resolveConditions(stmt.conditions) : void 0
132
+ });
133
+ }
134
+ function resolvePrincipals(principals) {
135
+ if (principals === "*") return [new iam.AnyPrincipal()];
136
+ return principals.flatMap((p) => {
137
+ switch (p.type) {
138
+ case "aws":
139
+ return p.identifiers.map((id) => new iam.ArnPrincipal(id));
140
+ case "service":
141
+ return p.identifiers.map((id) => new iam.ServicePrincipal(id));
142
+ case "federated":
143
+ return p.identifiers.map((id) => new iam.FederatedPrincipal(id, {}));
144
+ case "canonical":
145
+ return p.identifiers.map((id) => new iam.CanonicalUserPrincipal(id));
146
+ }
147
+ });
148
+ }
149
+ function resolveConditions(conditions) {
150
+ const result = {};
151
+ for (const c of conditions) {
152
+ const test = result[c.test] ??= {};
153
+ const existing = test[c.variable];
154
+ test[c.variable] = existing ? [...existing, ...c.values] : c.values;
155
+ }
156
+ return result;
157
+ }
158
+
159
+ // src/vpc.ts
160
+ import { Construct as Construct2 } from "constructs";
161
+ import { Stack } from "aws-cdk-lib";
162
+ import * as ec2 from "aws-cdk-lib/aws-ec2";
163
+ import * as iam2 from "aws-cdk-lib/aws-iam";
164
+ import * as logs from "aws-cdk-lib/aws-logs";
165
+ var Vpc2 = class extends Construct2 {
166
+ vpc;
167
+ publicSubnets;
168
+ privateSubnets;
169
+ securityGroup;
170
+ wireguard;
171
+ wireguardPublicKeyParam;
172
+ constructor(scope, id, props = {}) {
173
+ super(scope, id);
174
+ const maxAzs = props.maxAzs ?? 2;
175
+ const natOption = props.natGateways ?? "single";
176
+ const { natGateways, natGatewayProvider } = resolveNat(natOption, maxAzs);
177
+ const hasEgress = natOption !== "none";
178
+ const defaultSubnets = [
179
+ { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
180
+ {
181
+ name: "Private",
182
+ subnetType: hasEgress ? ec2.SubnetType.PRIVATE_WITH_EGRESS : ec2.SubnetType.PRIVATE_ISOLATED,
183
+ cidrMask: 24
184
+ }
185
+ ];
186
+ this.vpc = new ec2.Vpc(this, "Vpc", {
187
+ ipAddresses: ec2.IpAddresses.cidr(props.cidr ?? "10.0.0.0/16"),
188
+ maxAzs,
189
+ natGateways,
190
+ natGatewayProvider,
191
+ subnetConfiguration: props.subnetConfiguration ?? defaultSubnets
192
+ });
193
+ this.publicSubnets = this.vpc.publicSubnets;
194
+ this.privateSubnets = hasEgress ? this.vpc.privateSubnets : this.vpc.isolatedSubnets;
195
+ this.securityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
196
+ vpc: this.vpc,
197
+ description: `Default security group for ${id}`,
198
+ allowAllOutbound: true
199
+ });
200
+ if (props.wireguard) {
201
+ const region = Stack.of(this).region;
202
+ const privateKeyParam = `/wireguard/${id}/private-key`;
203
+ const publicKeyParam = `/wireguard/${id}/public-key`;
204
+ const peersParam = `/wireguard/${id}/peers`;
205
+ this.wireguardPublicKeyParam = publicKeyParam;
206
+ const wgSg = new ec2.SecurityGroup(this, "WireGuardSG", {
207
+ vpc: this.vpc,
208
+ description: "WireGuard VPN",
209
+ allowAllOutbound: true
210
+ });
211
+ wgSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.udp(51820), "WireGuard");
212
+ const vpcCidr = props.cidr ?? "10.0.0.0/16";
213
+ const userData = ec2.UserData.forLinux();
214
+ userData.addCommands(
215
+ // Install WireGuard
216
+ "dnf install -y wireguard-tools iptables-nft",
217
+ // Enable IP forwarding
218
+ "sysctl -w net.ipv4.ip_forward=1",
219
+ 'echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/99-wireguard.conf',
220
+ // Retrieve or generate server keys
221
+ "umask 077",
222
+ `if aws ssm get-parameter --name "${privateKeyParam}" --with-decryption --region ${region} --query "Parameter.Value" --output text > /etc/wireguard/server.key 2>/dev/null; then`,
223
+ " wg pubkey < /etc/wireguard/server.key > /etc/wireguard/server.pub",
224
+ "else",
225
+ " wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub",
226
+ ` aws ssm put-parameter --name "${privateKeyParam}" --type SecureString --value "$(cat /etc/wireguard/server.key)" --region ${region}`,
227
+ ` aws ssm put-parameter --name "${publicKeyParam}" --type String --value "$(cat /etc/wireguard/server.pub)" --region ${region}`,
228
+ "fi",
229
+ // Create wg0 config
230
+ "ENI=$(ip -4 route show default | awk '{print $5}')",
231
+ `cat > /etc/wireguard/wg0.conf << EOF`,
232
+ "[Interface]",
233
+ "Address = 10.100.0.1/24",
234
+ "ListenPort = 51820",
235
+ "PrivateKey = $(cat /etc/wireguard/server.key)",
236
+ "",
237
+ "PostUp = iptables -t nat -A POSTROUTING -s 10.100.0.0/24 -o $ENI -j MASQUERADE",
238
+ `PostUp = iptables -A FORWARD -s 10.100.0.0/24 -d ${vpcCidr} -j ACCEPT`,
239
+ "PostDown = iptables -t nat -D POSTROUTING -s 10.100.0.0/24 -o $ENI -j MASQUERADE",
240
+ `PostDown = iptables -D FORWARD -s 10.100.0.0/24 -d ${vpcCidr} -j ACCEPT`,
241
+ "EOF",
242
+ // Load persisted peers from SSM
243
+ `if aws ssm get-parameter --name "${peersParam}" --with-decryption --region ${region} --query "Parameter.Value" --output text >> /etc/wireguard/wg0.conf 2>/dev/null; then`,
244
+ ' echo "Loaded persisted peers"',
245
+ "fi",
246
+ // Enable and start WireGuard
247
+ "systemctl enable wg-quick@wg0",
248
+ "systemctl start wg-quick@wg0",
249
+ // Store metadata for CLI tooling
250
+ 'TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")',
251
+ 'PUBLIC_IP=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-ipv4)',
252
+ 'INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)',
253
+ `aws ssm put-parameter --name "/wireguard/${id}/endpoint" --type String --value "$PUBLIC_IP" --overwrite --region ${region}`,
254
+ `aws ssm put-parameter --name "/wireguard/${id}/instance-id" --type String --value "$INSTANCE_ID" --overwrite --region ${region}`
255
+ );
256
+ this.wireguard = new ec2.Instance(this, "WireGuard", {
257
+ vpc: this.vpc,
258
+ instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.NANO),
259
+ machineImage: ec2.MachineImage.latestAmazonLinux2023({ cpuType: ec2.AmazonLinuxCpuType.ARM_64 }),
260
+ vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
261
+ securityGroup: wgSg,
262
+ ssmSessionPermissions: true,
263
+ userData
264
+ });
265
+ this.wireguard.addSecurityGroup(this.securityGroup);
266
+ const ssmParamArn = Stack.of(this).formatArn({
267
+ service: "ssm",
268
+ resource: "parameter",
269
+ resourceName: `wireguard/${id}/*`
270
+ });
271
+ this.wireguard.role.addToPrincipalPolicy(
272
+ new iam2.PolicyStatement({
273
+ actions: ["ssm:GetParameter", "ssm:PutParameter"],
274
+ resources: [ssmParamArn]
275
+ })
276
+ );
277
+ const eip = new ec2.CfnEIP(this, "WireGuardEIP", {
278
+ instanceId: this.wireguard.instanceId
279
+ });
280
+ const gatewayAttachment = this.vpc.node.findChild("VPCGW");
281
+ eip.addDependency(gatewayAttachment);
282
+ }
283
+ if (props.flowLogs) {
284
+ const flowLogOpts = typeof props.flowLogs === "object" ? props.flowLogs : {};
285
+ const logGroup = new logs.LogGroup(this, "FlowLogGroup", {
286
+ retention: flowLogOpts.retention ?? logs.RetentionDays.ONE_MONTH
287
+ });
288
+ this.vpc.addFlowLog("FlowLog", {
289
+ destination: ec2.FlowLogDestination.toCloudWatchLogs(logGroup)
290
+ });
291
+ }
292
+ }
293
+ };
294
+ function resolveNat(option, maxAzs) {
295
+ switch (option) {
296
+ case "none":
297
+ return { natGateways: 0 };
298
+ case "ec2":
299
+ return {
300
+ natGateways: 1,
301
+ natGatewayProvider: ec2.NatProvider.instanceV2({
302
+ instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.NANO),
303
+ machineImage: new ec2.LookupMachineImage({
304
+ name: "fck-nat-al2023-*-arm64-ebs",
305
+ owners: ["568608671756"]
306
+ })
307
+ })
308
+ };
309
+ case "onePerAz":
310
+ return { natGateways: maxAzs };
311
+ case "single":
312
+ default:
313
+ return { natGateways: 1 };
314
+ }
315
+ }
316
+
317
+ // src/cluster.ts
318
+ import { Construct as Construct3 } from "constructs";
319
+ import * as ecs from "aws-cdk-lib/aws-ecs";
320
+ var Cluster2 = class extends Construct3 {
321
+ cluster;
322
+ constructor(scope, id, props) {
323
+ super(scope, id);
324
+ let clusterProps = {
325
+ vpc: props.vpc,
326
+ enableFargateCapacityProviders: props.enableFargateCapacityProviders ?? false
327
+ };
328
+ if (props.transform?.cluster) {
329
+ clusterProps = props.transform.cluster(clusterProps);
330
+ }
331
+ this.cluster = new ecs.Cluster(this, "Cluster", clusterProps);
332
+ }
333
+ };
334
+
335
+ // src/static-site.ts
336
+ import { Construct as Construct6 } from "constructs";
337
+ import { RemovalPolicy as RemovalPolicy2 } from "aws-cdk-lib";
338
+ import * as s33 from "aws-cdk-lib/aws-s3";
339
+ import * as cloudfront2 from "aws-cdk-lib/aws-cloudfront";
340
+ import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
341
+
342
+ // src/helpers/cloudfront.ts
343
+ import "constructs";
344
+ import "aws-cdk-lib/aws-s3";
345
+ import * as s3deploy from "aws-cdk-lib/aws-s3-deployment";
346
+ import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
347
+ import * as acm from "aws-cdk-lib/aws-certificatemanager";
348
+ function resolveCachePolicy(policy) {
349
+ if (!policy) return cloudfront.CachePolicy.CACHING_OPTIMIZED;
350
+ if (typeof policy === "string") {
351
+ switch (policy) {
352
+ case "CACHING_OPTIMIZED":
353
+ return cloudfront.CachePolicy.CACHING_OPTIMIZED;
354
+ case "CACHING_DISABLED":
355
+ return cloudfront.CachePolicy.CACHING_DISABLED;
356
+ }
357
+ }
358
+ return policy;
359
+ }
360
+ function resolveInvalidation(invalidation) {
361
+ if (invalidation === false) {
362
+ return { enabled: false, paths: [], wait: false };
363
+ }
364
+ if (invalidation === void 0) {
365
+ return { enabled: true, paths: ["/*"], wait: true };
366
+ }
367
+ const paths = invalidation.paths === "all" || invalidation.paths === void 0 ? ["/*"] : invalidation.paths;
368
+ return { enabled: true, paths, wait: invalidation.wait ?? true };
369
+ }
370
+ function resolveAssetDeployment(scope, id, opts) {
371
+ const invalidation = resolveInvalidation(opts.invalidation);
372
+ const baseDeployment = new s3deploy.BucketDeployment(scope, `${id}AssetDeployment`, {
373
+ sources: [s3deploy.Source.asset(opts.path)],
374
+ destinationBucket: opts.bucket,
375
+ destinationKeyPrefix: opts.prefix,
376
+ prune: true
377
+ });
378
+ opts.fileOptions.forEach((option, i) => {
379
+ const files = Array.isArray(option.files) ? option.files : [option.files];
380
+ const ignore = option.ignore ? Array.isArray(option.ignore) ? option.ignore : [option.ignore] : void 0;
381
+ const isLast = i === opts.fileOptions.length - 1;
382
+ const cacheControl = option.cacheControl ? s3deploy.CacheControl.fromString(option.cacheControl) : void 0;
383
+ const overlay = new s3deploy.BucketDeployment(scope, `${id}AssetDeploymentOverlay${i}`, {
384
+ sources: [s3deploy.Source.asset(opts.path)],
385
+ destinationBucket: opts.bucket,
386
+ destinationKeyPrefix: opts.prefix,
387
+ exclude: ["*", ...ignore ?? []],
388
+ include: files,
389
+ prune: false,
390
+ cacheControl: cacheControl ? [cacheControl] : void 0,
391
+ contentType: option.contentType,
392
+ distribution: isLast && invalidation.enabled ? opts.distribution : void 0,
393
+ distributionPaths: isLast && invalidation.enabled ? invalidation.paths : void 0
394
+ });
395
+ overlay.node.addDependency(baseDeployment);
396
+ });
397
+ }
398
+ function resolveCertificate(scope, domain) {
399
+ const certificate = domain ? acm.Certificate.fromCertificateArn(scope, "Certificate", domain.certificateArn) : void 0;
400
+ const domainNames = domain ? [domain.name, ...domain.aliases ?? []] : void 0;
401
+ return { certificate, domainNames };
402
+ }
403
+ function resolveAdditionalBehaviors(s3Origin, behaviors) {
404
+ const additionalBehaviors = {};
405
+ for (const behavior of behaviors) {
406
+ additionalBehaviors[behavior.pathPattern] = {
407
+ origin: s3Origin,
408
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
409
+ cachePolicy: resolveCachePolicy(behavior.cachePolicy)
410
+ };
411
+ }
412
+ return additionalBehaviors;
413
+ }
414
+ function createViewerRequestFunction(scope, id) {
415
+ return new cloudfront.Function(scope, id, {
416
+ code: cloudfront.FunctionCode.fromInline(`
417
+ function handler(event) {
418
+ var request = event.request;
419
+ var uri = request.uri;
420
+ var host = request.headers.host ? request.headers.host.value : '';
421
+ request.headers['x-forwarded-host'] = { value: host };
422
+ if (uri.length > 1 && uri.endsWith('/')) {
423
+ request.uri = uri.slice(0, -1);
424
+ }
425
+ return request;
426
+ }`)
427
+ });
428
+ }
429
+
430
+ // src/helpers/ecs.ts
431
+ import "constructs";
432
+ import * as logs2 from "aws-cdk-lib/aws-logs";
433
+ import * as ecs2 from "aws-cdk-lib/aws-ecs";
434
+ import * as ssm from "aws-cdk-lib/aws-ssm";
435
+ import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
436
+ function resolveLogRetention(retention) {
437
+ switch (retention) {
438
+ case "1 day":
439
+ return logs2.RetentionDays.ONE_DAY;
440
+ case "3 days":
441
+ return logs2.RetentionDays.THREE_DAYS;
442
+ case "5 days":
443
+ return logs2.RetentionDays.FIVE_DAYS;
444
+ case "1 week":
445
+ return logs2.RetentionDays.ONE_WEEK;
446
+ case "2 weeks":
447
+ return logs2.RetentionDays.TWO_WEEKS;
448
+ case "1 month":
449
+ case void 0:
450
+ return logs2.RetentionDays.ONE_MONTH;
451
+ case "3 months":
452
+ return logs2.RetentionDays.THREE_MONTHS;
453
+ case "6 months":
454
+ return logs2.RetentionDays.SIX_MONTHS;
455
+ case "1 year":
456
+ return logs2.RetentionDays.ONE_YEAR;
457
+ case "2 years":
458
+ return logs2.RetentionDays.TWO_YEARS;
459
+ case "forever":
460
+ return logs2.RetentionDays.INFINITE;
461
+ }
462
+ }
463
+ function resolveSecrets(scope, ssmEntries, explicitSecrets, prefix) {
464
+ const merged = {};
465
+ let hasEntries = false;
466
+ const p = prefix ? `${prefix}-` : "";
467
+ if (ssmEntries) {
468
+ let i = 0;
469
+ for (const [envVar, arn] of Object.entries(ssmEntries)) {
470
+ hasEntries = true;
471
+ if (arn.startsWith("arn:aws:secretsmanager:")) {
472
+ merged[envVar] = ecs2.Secret.fromSecretsManager(
473
+ secretsmanager.Secret.fromSecretCompleteArn(scope, `${p}Secret${i}`, arn)
474
+ );
475
+ } else {
476
+ merged[envVar] = ecs2.Secret.fromSsmParameter(
477
+ ssm.StringParameter.fromSecureStringParameterAttributes(scope, `${p}SsmParam${i}`, { parameterName: arn })
478
+ );
479
+ }
480
+ i++;
481
+ }
482
+ }
483
+ if (explicitSecrets) {
484
+ hasEntries = true;
485
+ Object.assign(merged, explicitSecrets);
486
+ }
487
+ return hasEntries ? merged : void 0;
488
+ }
489
+
490
+ // src/helpers/docker.ts
491
+ import { execSync } from "child_process";
492
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
493
+ import { dirname, relative, resolve } from "path";
494
+ function findMonorepoRoot(startDir) {
495
+ let dir = startDir;
496
+ while (true) {
497
+ if (existsSync(resolve(dir, "pnpm-workspace.yaml"))) return dir;
498
+ const parent = dirname(dir);
499
+ if (parent === dir) return null;
500
+ dir = parent;
501
+ }
502
+ }
503
+ function readPackageName(appRoot) {
504
+ const raw = readFileSync(resolve(appRoot, "package.json"), "utf-8");
505
+ const pkg = JSON.parse(raw);
506
+ if (!pkg.name) {
507
+ throw new Error(`package.json at "${appRoot}" is missing a "name" field`);
508
+ }
509
+ return pkg.name;
510
+ }
511
+ function buildMonorepoDockerfile(buildDir, port) {
512
+ return `FROM node:22-slim
513
+ WORKDIR /app
514
+ ENV NODE_ENV=production
515
+ COPY package.json .
516
+ COPY node_modules/ ./node_modules/
517
+ COPY ${buildDir}/server/ ./${buildDir}/server/
518
+ EXPOSE ${port}
519
+ CMD ["./node_modules/.bin/react-router-serve", "./${buildDir}/server/index.js"]
520
+ `;
521
+ }
522
+ function resolveReactRouterDockerContext(opts) {
523
+ const { appRoot, buildDir, basePath, port, className } = opts;
524
+ const serverBuild = `${basePath}/server/index.js`;
525
+ if (!existsSync(serverBuild)) {
526
+ throw new Error(`${className}: "${serverBuild}" not found. Run "react-router build" before deploying.`);
527
+ }
528
+ if (existsSync(`${appRoot}/Dockerfile`)) {
529
+ return { context: appRoot };
530
+ }
531
+ const absAppRoot = resolve(appRoot);
532
+ const monorepoRoot = findMonorepoRoot(absAppRoot);
533
+ if (monorepoRoot) {
534
+ const packageName = readPackageName(absAppRoot);
535
+ const deployDir = resolve(absAppRoot, buildDir, "_deploy");
536
+ rmSync(deployDir, { recursive: true, force: true });
537
+ execSync(`pnpm --filter "${packageName}" deploy --prod ${deployDir}`, {
538
+ cwd: monorepoRoot,
539
+ stdio: "inherit"
540
+ });
541
+ const destBuildDir = resolve(deployDir, buildDir, "server");
542
+ mkdirSync(destBuildDir, { recursive: true });
543
+ cpSync(resolve(absAppRoot, buildDir, "server"), destBuildDir, { recursive: true });
544
+ writeFileSync(resolve(deployDir, "Dockerfile"), buildMonorepoDockerfile(buildDir, port));
545
+ return { context: relative(".", deployDir) || "." };
546
+ }
547
+ writeFileSync(`${basePath}/_Dockerfile`, buildStandaloneDockerfile(buildDir, port));
548
+ return { context: appRoot, dockerfile: `${buildDir}/_Dockerfile` };
549
+ }
550
+ function buildStandaloneDockerfile(buildDir, port) {
551
+ return `# syntax=docker/dockerfile:1.5
552
+
553
+ FROM node:22-slim AS base
554
+ WORKDIR /app
555
+ ENV CI=true
556
+ RUN corepack enable
557
+
558
+ COPY package.json pnpm-lock.yaml ./
559
+ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prod
560
+
561
+ COPY ${buildDir}/ ./${buildDir}/
562
+
563
+ ENV NODE_ENV=production
564
+ EXPOSE ${port}
565
+ CMD ["./node_modules/.bin/react-router-serve", "./${buildDir}/server/index.js"]
566
+ `;
567
+ }
568
+
569
+ // src/static-site.ts
570
+ var defaultFileOptions = [
571
+ { files: "**", cacheControl: "max-age=31536000,public,immutable" },
572
+ { files: "**/*.html", cacheControl: "max-age=0,no-cache,no-store,must-revalidate" }
573
+ ];
574
+ var StaticSite = class extends Construct6 {
575
+ bucket;
576
+ distribution;
577
+ constructor(scope, id, props) {
578
+ super(scope, id);
579
+ const indexPage = props.indexPage ?? "index.html";
580
+ const errorPage = props.errorPage ?? indexPage;
581
+ const fileOptions = props.assets?.fileOptions ?? defaultFileOptions;
582
+ let bucketProps = {
583
+ blockPublicAccess: s33.BlockPublicAccess.BLOCK_ALL,
584
+ removalPolicy: RemovalPolicy2.RETAIN
585
+ };
586
+ if (props.transform?.bucket) {
587
+ bucketProps = props.transform.bucket(bucketProps);
588
+ }
589
+ this.bucket = new s33.Bucket(this, "Bucket", bucketProps);
590
+ const { certificate, domainNames } = resolveCertificate(this, props.domain);
591
+ const functionAssociations = [];
592
+ if (props.edge?.viewerRequest) {
593
+ functionAssociations.push({
594
+ function: props.edge.viewerRequest,
595
+ eventType: cloudfront2.FunctionEventType.VIEWER_REQUEST
596
+ });
597
+ }
598
+ if (props.edge?.viewerResponse) {
599
+ functionAssociations.push({
600
+ function: props.edge.viewerResponse,
601
+ eventType: cloudfront2.FunctionEventType.VIEWER_RESPONSE
602
+ });
603
+ }
604
+ let distributionProps = {
605
+ defaultBehavior: {
606
+ origin: origins.S3BucketOrigin.withOriginAccessControl(this.bucket),
607
+ viewerProtocolPolicy: cloudfront2.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
608
+ functionAssociations: functionAssociations.length > 0 ? functionAssociations : void 0
609
+ },
610
+ defaultRootObject: indexPage,
611
+ domainNames,
612
+ certificate,
613
+ errorResponses: [
614
+ { httpStatus: 403, responseHttpStatus: 200, responsePagePath: `/${errorPage}` },
615
+ { httpStatus: 404, responseHttpStatus: 200, responsePagePath: `/${errorPage}` }
616
+ ]
617
+ };
618
+ if (props.transform?.distribution) {
619
+ distributionProps = props.transform.distribution(distributionProps);
620
+ }
621
+ this.distribution = new cloudfront2.Distribution(this, "Distribution", distributionProps);
622
+ resolveAssetDeployment(this, "", {
623
+ path: props.path,
624
+ bucket: this.bucket,
625
+ fileOptions,
626
+ distribution: this.distribution,
627
+ invalidation: props.invalidation
628
+ });
629
+ }
630
+ };
631
+
632
+ // src/react-router-ssr-site-lambda.ts
633
+ import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
634
+ import { Construct as Construct9 } from "constructs";
635
+
636
+ // src/helpers/ssr-site-lambda.ts
637
+ import "constructs";
638
+ import { Duration as Duration3, RemovalPolicy as RemovalPolicy3, Stack as Stack2, SymlinkFollowMode } from "aws-cdk-lib";
639
+ import * as lambda2 from "aws-cdk-lib/aws-lambda";
640
+ import * as s34 from "aws-cdk-lib/aws-s3";
641
+ import * as cloudfront3 from "aws-cdk-lib/aws-cloudfront";
642
+ import * as origins2 from "aws-cdk-lib/aws-cloudfront-origins";
643
+ import * as iam4 from "aws-cdk-lib/aws-iam";
644
+
645
+ // src/lambda-function.ts
646
+ import { Construct as Construct7 } from "constructs";
647
+ import { Duration as Duration2, Tags } from "aws-cdk-lib";
648
+ import * as lambda from "aws-cdk-lib/aws-lambda";
649
+ import * as lambdaNode from "aws-cdk-lib/aws-lambda-nodejs";
650
+ import * as logs3 from "aws-cdk-lib/aws-logs";
651
+ import * as iam3 from "aws-cdk-lib/aws-iam";
652
+ var ESM_BANNER = [
653
+ 'import { createRequire as __banner_require__ } from "module";',
654
+ 'import { fileURLToPath as __banner_fileURLToPath__ } from "url";',
655
+ 'import { dirname as __banner_dirname__ } from "path";',
656
+ "const require = __banner_require__(import.meta.url);",
657
+ "const __filename = __banner_fileURLToPath__(import.meta.url);",
658
+ "const __dirname = __banner_dirname__(__filename);"
659
+ ].join(" ");
660
+ var LambdaFunction = class extends Construct7 {
661
+ function;
662
+ logGroup;
663
+ url;
664
+ version;
665
+ alias;
666
+ constructor(scope, id, props) {
667
+ super(scope, id);
668
+ if (props.bundle && props.code) {
669
+ throw new Error('Function: cannot set both "bundle" and "code" \u2014 use one or the other');
670
+ }
671
+ if (props.streaming && !props.url) {
672
+ throw new Error('Function: "streaming" requires "url" to be enabled');
673
+ }
674
+ const code = props.bundle ? lambda.Code.fromAsset(props.bundle) : props.code;
675
+ const runtime = props.runtime ?? lambda.Runtime.NODEJS_22_X;
676
+ const architecture = props.architecture ?? lambda.Architecture.ARM_64;
677
+ const memorySize = props.memory ?? 1024;
678
+ const timeout = props.timeout ?? Duration2.seconds(20);
679
+ const loggingFormat = props.logging?.format === "json" ? lambda.LoggingFormat.JSON : lambda.LoggingFormat.TEXT;
680
+ let logGroupProps = {
681
+ retention: resolveLogRetention(props.logging?.retention)
682
+ };
683
+ if (props.transform?.logGroup) {
684
+ logGroupProps = props.transform.logGroup(logGroupProps);
685
+ }
686
+ this.logGroup = new logs3.LogGroup(this, "LogGroup", logGroupProps);
687
+ const vpcConfig = resolveVpc(props.vpc);
688
+ if (code) {
689
+ let fnProps = {
690
+ code,
691
+ handler: props.handler,
692
+ runtime,
693
+ architecture,
694
+ memorySize,
695
+ timeout,
696
+ ephemeralStorageSize: props.storage,
697
+ description: props.description,
698
+ environment: props.environment,
699
+ layers: props.layers,
700
+ logGroup: this.logGroup,
701
+ loggingFormat,
702
+ vpc: vpcConfig.vpc,
703
+ vpcSubnets: vpcConfig.subnets,
704
+ securityGroups: vpcConfig.securityGroups
705
+ };
706
+ if (props.transform?.function) {
707
+ fnProps = props.transform.function(fnProps);
708
+ }
709
+ this.function = new lambda.Function(this, "Function", fnProps);
710
+ } else {
711
+ const parsed = parseHandler(props.handler);
712
+ const entry = resolveEntry(parsed.entry);
713
+ const nodejsOpts = props.nodejs ?? {};
714
+ const format = nodejsOpts.format ?? lambdaNode.OutputFormat.ESM;
715
+ const isEsm = format === lambdaNode.OutputFormat.ESM;
716
+ const banner = nodejsOpts.banner ?? (isEsm ? ESM_BANNER : void 0);
717
+ const defaultExclude = ["@aws-sdk/*"];
718
+ const exclude = nodejsOpts.exclude ?? defaultExclude;
719
+ const install = nodejsOpts.install ?? [];
720
+ const afterBundlingCommands = [];
721
+ if (nodejsOpts.copyFiles) {
722
+ afterBundlingCommands.push(...resolveCopyFiles(nodejsOpts.copyFiles));
723
+ }
724
+ const hasCommandHooks = afterBundlingCommands.length > 0 || props.hook?.postBuild;
725
+ let fnProps = {
726
+ entry,
727
+ handler: parsed.export,
728
+ runtime,
729
+ architecture,
730
+ memorySize,
731
+ timeout,
732
+ ephemeralStorageSize: props.storage,
733
+ description: props.description,
734
+ environment: props.environment,
735
+ layers: props.layers,
736
+ logGroup: this.logGroup,
737
+ loggingFormat,
738
+ vpc: vpcConfig.vpc,
739
+ vpcSubnets: vpcConfig.subnets,
740
+ securityGroups: vpcConfig.securityGroups,
741
+ bundling: {
742
+ minify: nodejsOpts.minify ?? true,
743
+ sourceMap: nodejsOpts.sourceMap ?? false,
744
+ format,
745
+ target: "es2023",
746
+ banner,
747
+ externalModules: [...exclude, ...install],
748
+ nodeModules: install.length > 0 ? install : void 0,
749
+ loader: nodejsOpts.loader,
750
+ esbuildArgs: nodejsOpts.esbuild,
751
+ commandHooks: hasCommandHooks ? {
752
+ beforeBundling: () => [],
753
+ beforeInstall: () => [],
754
+ afterBundling: (_inputDir, outputDir) => [
755
+ ...afterBundlingCommands.map((cmd) => cmd.replace("$out", outputDir)),
756
+ ...props.hook?.postBuild?.(outputDir) ?? []
757
+ ]
758
+ } : void 0
759
+ }
760
+ };
761
+ if (props.transform?.function) {
762
+ fnProps = props.transform.function(fnProps);
763
+ }
764
+ this.function = new lambdaNode.NodejsFunction(this, "Function", fnProps);
765
+ }
766
+ if (props.permissions) {
767
+ for (const perm of props.permissions) {
768
+ this.function.addToRolePolicy(new iam3.PolicyStatement({ actions: perm.actions, resources: perm.resources }));
769
+ }
770
+ }
771
+ if (props.policies) {
772
+ for (const arn of props.policies) {
773
+ this.function.role.addManagedPolicy(iam3.ManagedPolicy.fromManagedPolicyArn(this, `Policy-${arn}`, arn));
774
+ }
775
+ }
776
+ if (props.retries !== void 0) {
777
+ new lambda.EventInvokeConfig(this, "EventInvokeConfig", {
778
+ function: this.function,
779
+ retryAttempts: props.retries
780
+ });
781
+ }
782
+ if (props.versioning || props.concurrency?.provisioned) {
783
+ ;
784
+ this.version = this.function.currentVersion;
785
+ }
786
+ if (props.concurrency?.reserved !== void 0) {
787
+ const cfnFunction = this.function.node.defaultChild;
788
+ cfnFunction.addPropertyOverride("ReservedConcurrentExecutions", props.concurrency.reserved);
789
+ }
790
+ if (props.concurrency?.provisioned) {
791
+ const version = this.version ?? this.function.currentVersion;
792
+ const alias = new lambda.Alias(this, "Alias", {
793
+ aliasName: "live",
794
+ version,
795
+ provisionedConcurrentExecutions: props.concurrency.provisioned
796
+ });
797
+ this.version = version;
798
+ this.alias = alias;
799
+ }
800
+ if (props.tags) {
801
+ for (const [key, value] of Object.entries(props.tags)) {
802
+ Tags.of(this).add(key, value);
803
+ }
804
+ }
805
+ if (props.url) {
806
+ const urlOpts = resolveUrl(props.url);
807
+ this.url = this.function.addFunctionUrl({
808
+ authType: urlOpts.authorization === "iam" ? lambda.FunctionUrlAuthType.AWS_IAM : lambda.FunctionUrlAuthType.NONE,
809
+ cors: urlOpts.cors === true ? { allowedOrigins: ["*"], allowedMethods: [lambda.HttpMethod.ALL] } : urlOpts.cors,
810
+ invokeMode: props.streaming ? lambda.InvokeMode.RESPONSE_STREAM : void 0
811
+ });
812
+ }
813
+ }
814
+ };
815
+ function parseHandler(handler) {
816
+ const lastDot = handler.lastIndexOf(".");
817
+ if (lastDot === -1) {
818
+ throw new Error(
819
+ `Function: handler "${handler}" must contain a dot separating the file path from the export name (e.g. "src/api.handler")`
820
+ );
821
+ }
822
+ return {
823
+ entry: handler.slice(0, lastDot),
824
+ export: handler.slice(lastDot + 1)
825
+ };
826
+ }
827
+ function resolveEntry(filePath) {
828
+ const lastSlash = filePath.lastIndexOf("/");
829
+ const basename = lastSlash === -1 ? filePath : filePath.slice(lastSlash + 1);
830
+ if (basename.includes(".")) return filePath;
831
+ return filePath + ".ts";
832
+ }
833
+ function resolveVpc(vpc) {
834
+ if (!vpc) return {};
835
+ if ("vpc" in vpc) return vpc;
836
+ return { vpc };
837
+ }
838
+ function resolveUrl(url) {
839
+ if (url === true) return { authorization: "none" };
840
+ if (!url) return { authorization: "none" };
841
+ return {
842
+ authorization: url.authorization ?? "none",
843
+ cors: url.cors
844
+ };
845
+ }
846
+ function resolveCopyFiles(copyFiles) {
847
+ return copyFiles.map((f) => {
848
+ const dest = f.to ?? f.from;
849
+ return `cp -r ${f.from} $out/${dest}`;
850
+ });
851
+ }
852
+
853
+ // src/helpers/ssr-site-lambda.ts
854
+ var defaultAssetBehaviors = [{ pathPattern: "/_assets/*", cachePolicy: "CACHING_OPTIMIZED" }];
855
+ function createSsrSiteLambda(scope, props) {
856
+ if (props.server.nodejs && props.server.code) {
857
+ throw new Error("SsrSiteLambda: server.nodejs and server.code are mutually exclusive");
858
+ }
859
+ if (!props.server.code && !props.server.path) {
860
+ throw new Error("SsrSiteLambda: either server.code or server.path must be provided");
861
+ }
862
+ const useBundling = !!props.server.nodejs;
863
+ const assetBehaviors = props.assets?.behaviors ?? defaultAssetBehaviors;
864
+ const handler = props.server.handler ?? "index.handler";
865
+ const serverCode = useBundling ? void 0 : props.server.code ?? lambda2.Code.fromAsset(props.server.path, { followSymlinks: SymlinkFollowMode.ALWAYS });
866
+ const fn = new LambdaFunction(scope, "Server", {
867
+ code: serverCode,
868
+ handler: useBundling ? resolveBundlingHandler(handler, props.server.path) : handler,
869
+ nodejs: useBundling ? props.server.nodejs : void 0,
870
+ runtime: props.server.runtime ?? lambda2.Runtime.NODEJS_22_X,
871
+ memory: props.server.memory ?? 1024,
872
+ timeout: props.server.timeout ?? Duration3.seconds(30),
873
+ architecture: props.server.architecture ?? lambda2.Architecture.ARM_64,
874
+ environment: props.environment,
875
+ vpc: props.vpc,
876
+ permissions: props.permissions,
877
+ layers: props.server.layers,
878
+ streaming: props.streaming,
879
+ url: { authorization: "iam" },
880
+ transform: props.transform?.lambda ? { function: props.transform.lambda } : void 0
881
+ });
882
+ const fnResource = fn.function;
883
+ const functionUrl = fn.url;
884
+ let bucketProps = {
885
+ blockPublicAccess: s34.BlockPublicAccess.BLOCK_ALL,
886
+ removalPolicy: RemovalPolicy3.RETAIN
887
+ };
888
+ if (props.transform?.bucket) {
889
+ bucketProps = props.transform.bucket(bucketProps);
890
+ }
891
+ const bucket = new s34.Bucket(scope, "Bucket", bucketProps);
892
+ const cfFunction = props.edge?.viewerRequest ?? createViewerRequestFunction(scope, "ViewerRequestFn");
893
+ const defaultFunctionAssociations = [
894
+ { function: cfFunction, eventType: cloudfront3.FunctionEventType.VIEWER_REQUEST }
895
+ ];
896
+ if (props.edge?.viewerResponse) {
897
+ defaultFunctionAssociations.push({
898
+ function: props.edge.viewerResponse,
899
+ eventType: cloudfront3.FunctionEventType.VIEWER_RESPONSE
900
+ });
901
+ }
902
+ const { certificate, domainNames } = resolveCertificate(scope, props.domain);
903
+ const s3Origin = origins2.S3BucketOrigin.withOriginAccessControl(bucket);
904
+ const lambdaOrigin = origins2.FunctionUrlOrigin.withOriginAccessControl(functionUrl);
905
+ const additionalBehaviors = resolveAdditionalBehaviors(s3Origin, assetBehaviors);
906
+ let distributionProps = {
907
+ defaultBehavior: {
908
+ origin: lambdaOrigin,
909
+ viewerProtocolPolicy: cloudfront3.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
910
+ cachePolicy: cloudfront3.CachePolicy.CACHING_DISABLED,
911
+ originRequestPolicy: cloudfront3.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
912
+ allowedMethods: cloudfront3.AllowedMethods.ALLOW_ALL,
913
+ functionAssociations: defaultFunctionAssociations
914
+ },
915
+ additionalBehaviors,
916
+ domainNames,
917
+ certificate
918
+ };
919
+ if (props.transform?.distribution) {
920
+ distributionProps = props.transform.distribution(distributionProps);
921
+ }
922
+ const distribution = new cloudfront3.Distribution(scope, "Distribution", distributionProps);
923
+ fnResource.addPermission("CloudFrontInvokeFunction", {
924
+ principal: new iam4.ServicePrincipal("cloudfront.amazonaws.com"),
925
+ action: "lambda:InvokeFunction",
926
+ sourceArn: `arn:aws:cloudfront::${Stack2.of(scope).account}:distribution/${distribution.distributionId}`
927
+ });
928
+ bucket.grantRead(fnResource);
929
+ if (props.assets?.path) {
930
+ const fileOptions = props.assets.fileOptions ?? [{ files: "**", cacheControl: "max-age=31536000,public,immutable" }];
931
+ resolveAssetDeployment(scope, "", {
932
+ path: props.assets.path,
933
+ bucket,
934
+ prefix: props.assets.prefix,
935
+ fileOptions,
936
+ distribution,
937
+ invalidation: props.invalidation
938
+ });
939
+ }
940
+ return { lambda: fnResource, functionUrl, bucket, distribution };
941
+ }
942
+ function resolveBundlingHandler(handler, serverPath) {
943
+ const lastDot = handler.lastIndexOf(".");
944
+ if (lastDot === -1) return `${serverPath}/${handler}`;
945
+ const file = handler.slice(0, lastDot);
946
+ const exportName = handler.slice(lastDot + 1);
947
+ return `${serverPath}/${file}.js.${exportName}`;
948
+ }
949
+
950
+ // src/react-router-ssr-site-lambda.ts
951
+ var ReactRouterSsrSiteLambda = class extends Construct9 {
952
+ lambda;
953
+ functionUrl;
954
+ bucket;
955
+ distribution;
956
+ constructor(scope, id, props) {
957
+ super(scope, id);
958
+ const basePath = !props.path || props.path === "." ? props.buildDirectory ?? "build" : `${props.path}/${props.buildDirectory ?? "build"}`;
959
+ const serverBuild = `${basePath}/server/index.js`;
960
+ if (!existsSync2(serverBuild)) {
961
+ throw new Error(`ReactRouterSsrSiteLambda: "${serverBuild}" not found. Run "react-router build" before deploying.`);
962
+ }
963
+ const mode = props.server?.mode ?? "streaming";
964
+ const handlerSource = buildHandlerSource("./server/index.js", mode);
965
+ writeFileSync2(`${basePath}/_lambda-handler.js`, handlerSource);
966
+ const external = props.server?.external ?? [];
967
+ const merged = {
968
+ streaming: mode === "streaming",
969
+ environment: props.environment,
970
+ vpc: props.vpc,
971
+ permissions: props.permissions,
972
+ domain: props.domain,
973
+ edge: props.edge,
974
+ invalidation: props.invalidation,
975
+ transform: props.transform,
976
+ server: {
977
+ path: basePath,
978
+ handler: "_lambda-handler.handler",
979
+ runtime: props.server?.runtime,
980
+ memory: props.server?.memory,
981
+ timeout: props.server?.timeout,
982
+ architecture: props.server?.architecture,
983
+ layers: props.server?.layers,
984
+ nodejs: {
985
+ install: props.server?.install,
986
+ exclude: ["@aws-sdk/*", ...external]
987
+ }
988
+ },
989
+ assets: {
990
+ ...props.assets,
991
+ path: props.assets?.path ?? `${basePath}/client`,
992
+ behaviors: props.assets?.behaviors ?? [{ pathPattern: "/assets/*", cachePolicy: "CACHING_OPTIMIZED" }],
993
+ fileOptions: props.assets?.fileOptions ?? [{ files: "**", cacheControl: "max-age=31536000,public,immutable" }]
994
+ }
995
+ };
996
+ const resources = createSsrSiteLambda(this, merged);
997
+ this.lambda = resources.lambda;
998
+ this.functionUrl = resources.functionUrl;
999
+ this.bucket = resources.bucket;
1000
+ this.distribution = resources.distribution;
1001
+ }
1002
+ };
1003
+ function buildHandlerSource(serverBuildPath, mode) {
1004
+ const shared = `
1005
+ import { createRequestHandler } from 'react-router'
1006
+ import * as build from '${serverBuildPath}'
1007
+
1008
+ const handleRequest = createRequestHandler(build, process.env.NODE_ENV)
1009
+
1010
+ function createRequest(event) {
1011
+ var host = event.headers['x-forwarded-host'] || event.headers.host
1012
+ var search = event.rawQueryString.length ? '?' + event.rawQueryString : ''
1013
+ var scheme = event.headers['x-forwarded-proto'] || 'http'
1014
+ var url = new URL(event.rawPath + search, scheme + '://' + host)
1015
+ var isFormData = event.headers['content-type']?.includes('multipart/form-data')
1016
+
1017
+ return new Request(url.href, {
1018
+ method: event.requestContext.http.method,
1019
+ headers: createHeaders(event.headers, event.cookies),
1020
+ signal: new AbortController().signal,
1021
+ body:
1022
+ event.body && event.isBase64Encoded
1023
+ ? isFormData
1024
+ ? Buffer.from(event.body, 'base64')
1025
+ : Buffer.from(event.body, 'base64').toString()
1026
+ : event.body
1027
+ })
1028
+ }
1029
+
1030
+ function createHeaders(requestHeaders, requestCookies) {
1031
+ var headers = new Headers()
1032
+ for (var [header, value] of Object.entries(requestHeaders)) {
1033
+ if (value) {
1034
+ headers.append(header, value)
1035
+ }
1036
+ }
1037
+ if (requestCookies) {
1038
+ headers.append('Cookie', requestCookies.join('; '))
1039
+ }
1040
+ return headers
1041
+ }
1042
+ `;
1043
+ if (mode === "streaming") {
1044
+ return shared + `
1045
+ async function writeReadableStreamToWritable(readableStream, writable) {
1046
+ var reader = readableStream.getReader()
1047
+ try {
1048
+ while (true) {
1049
+ var { done, value } = await reader.read()
1050
+ if (done) break
1051
+ writable.write(value)
1052
+ }
1053
+ } finally {
1054
+ reader.releaseLock()
1055
+ writable.end()
1056
+ }
1057
+ }
1058
+
1059
+ async function sendResponse(response, responseStream) {
1060
+ var cookies = response.headers.getSetCookie()
1061
+ if (cookies.length) {
1062
+ response.headers.delete('Set-Cookie')
1063
+ }
1064
+
1065
+ var metadata = {
1066
+ statusCode: response.status,
1067
+ headers: Object.fromEntries(response.headers.entries()),
1068
+ cookies: cookies
1069
+ }
1070
+
1071
+ var body =
1072
+ response.body ??
1073
+ new ReadableStream({
1074
+ start(controller) {
1075
+ controller.enqueue(new Uint8Array(0))
1076
+ controller.close()
1077
+ }
1078
+ })
1079
+
1080
+ var httpResponseStream = awslambda.HttpResponseStream.from(responseStream, metadata)
1081
+ await writeReadableStreamToWritable(body, httpResponseStream)
1082
+ }
1083
+
1084
+ export var handler = awslambda.streamifyResponse(async function(event, responseStream) {
1085
+ var request
1086
+ try {
1087
+ request = createRequest(event)
1088
+ } catch (e) {
1089
+ return await sendResponse(
1090
+ new Response('Bad Request: ' + (e instanceof Error ? e.message : e), { status: 400 }),
1091
+ responseStream
1092
+ )
1093
+ }
1094
+
1095
+ var response = await handleRequest(request)
1096
+ return await sendResponse(response, responseStream)
1097
+ })
1098
+ `;
1099
+ }
1100
+ return shared + `
1101
+ async function buildBufferedResponse(response) {
1102
+ var cookies = response.headers.getSetCookie()
1103
+ if (cookies.length) {
1104
+ response.headers.delete('Set-Cookie')
1105
+ }
1106
+
1107
+ var contentType = response.headers.get('Content-Type')
1108
+ var isBinary = contentType && !contentType.startsWith('text/') && !contentType.includes('json')
1109
+
1110
+ var body
1111
+ var isBase64Encoded = false
1112
+
1113
+ if (isBinary) {
1114
+ var arrayBuffer = await response.arrayBuffer()
1115
+ body = Buffer.from(arrayBuffer).toString('base64')
1116
+ isBase64Encoded = true
1117
+ } else {
1118
+ body = await response.text()
1119
+ }
1120
+
1121
+ return {
1122
+ statusCode: response.status,
1123
+ headers: Object.fromEntries(response.headers.entries()),
1124
+ cookies: cookies,
1125
+ body: body,
1126
+ isBase64Encoded: isBase64Encoded
1127
+ }
1128
+ }
1129
+
1130
+ export async function handler(event) {
1131
+ var request
1132
+ try {
1133
+ request = createRequest(event)
1134
+ } catch (e) {
1135
+ return buildBufferedResponse(
1136
+ new Response('Bad Request: ' + (e instanceof Error ? e.message : e), { status: 400 })
1137
+ )
1138
+ }
1139
+
1140
+ var response = await handleRequest(request)
1141
+ return buildBufferedResponse(response)
1142
+ }
1143
+ `;
1144
+ }
1145
+
1146
+ // src/react-router-ssr-site-ecs-fargate-service.ts
1147
+ import { Construct as Construct12 } from "constructs";
1148
+
1149
+ // src/helpers/ssr-site-ecs-service.ts
1150
+ import "constructs";
1151
+ import { RemovalPolicy as RemovalPolicy4 } from "aws-cdk-lib";
1152
+ import * as s35 from "aws-cdk-lib/aws-s3";
1153
+ import * as cloudfront4 from "aws-cdk-lib/aws-cloudfront";
1154
+ import * as origins3 from "aws-cdk-lib/aws-cloudfront-origins";
1155
+ import * as iam6 from "aws-cdk-lib/aws-iam";
1156
+
1157
+ // src/ecs-fargate-service.ts
1158
+ import { Construct as Construct10 } from "constructs";
1159
+ import { Duration as Duration4 } from "aws-cdk-lib";
1160
+ import * as ec22 from "aws-cdk-lib/aws-ec2";
1161
+ import * as ecs3 from "aws-cdk-lib/aws-ecs";
1162
+ import * as ecrAssets from "aws-cdk-lib/aws-ecr-assets";
1163
+ import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
1164
+ import * as logs4 from "aws-cdk-lib/aws-logs";
1165
+ import * as iam5 from "aws-cdk-lib/aws-iam";
1166
+ import * as acm2 from "aws-cdk-lib/aws-certificatemanager";
1167
+ var EcsFargateService = class extends Construct10 {
1168
+ service;
1169
+ taskDefinition;
1170
+ containers;
1171
+ logGroups;
1172
+ securityGroup;
1173
+ alb;
1174
+ listener;
1175
+ targetGroups;
1176
+ constructor(scope, id, props) {
1177
+ super(scope, id);
1178
+ if (!props.containers.length) {
1179
+ throw new Error("At least one container is required");
1180
+ }
1181
+ const cpu = props.cpu ?? 256;
1182
+ const memoryLimitMiB = props.memoryLimitMiB ?? 512;
1183
+ const arch = props.architecture ?? "X86_64";
1184
+ const cpuArchitecture = arch === "ARM64" ? ecs3.CpuArchitecture.ARM64 : ecs3.CpuArchitecture.X86_64;
1185
+ const platform = arch === "ARM64" ? ecrAssets.Platform.LINUX_ARM64 : ecrAssets.Platform.LINUX_AMD64;
1186
+ let executionRoleProps = {
1187
+ assumedBy: new iam5.ServicePrincipal("ecs-tasks.amazonaws.com"),
1188
+ managedPolicies: [iam5.ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonECSTaskExecutionRolePolicy")]
1189
+ };
1190
+ if (props.transform?.executionRole) {
1191
+ executionRoleProps = props.transform.executionRole(executionRoleProps);
1192
+ }
1193
+ const executionRole = new iam5.Role(this, "ExecutionRole", executionRoleProps);
1194
+ let taskRoleProps = {
1195
+ assumedBy: new iam5.ServicePrincipal("ecs-tasks.amazonaws.com")
1196
+ };
1197
+ if (props.transform?.taskRole) {
1198
+ taskRoleProps = props.transform.taskRole(taskRoleProps);
1199
+ }
1200
+ const taskRole = new iam5.Role(this, "TaskRole", taskRoleProps);
1201
+ let taskDefProps = {
1202
+ cpu,
1203
+ memoryLimitMiB,
1204
+ executionRole,
1205
+ taskRole,
1206
+ runtimePlatform: {
1207
+ cpuArchitecture,
1208
+ operatingSystemFamily: ecs3.OperatingSystemFamily.LINUX
1209
+ }
1210
+ };
1211
+ if (props.transform?.taskDefinition) {
1212
+ taskDefProps = props.transform.taskDefinition(taskDefProps);
1213
+ }
1214
+ this.taskDefinition = new ecs3.FargateTaskDefinition(this, "TaskDefinition", taskDefProps);
1215
+ if (props.permissions) {
1216
+ for (const perm of props.permissions) {
1217
+ this.taskDefinition.taskRole.addToPrincipalPolicy(
1218
+ new iam5.PolicyStatement({
1219
+ actions: perm.actions,
1220
+ resources: perm.resources,
1221
+ effect: perm.effect === "deny" ? iam5.Effect.DENY : iam5.Effect.ALLOW,
1222
+ conditions: perm.conditions
1223
+ })
1224
+ );
1225
+ }
1226
+ }
1227
+ this.containers = {};
1228
+ this.logGroups = {};
1229
+ for (const c of props.containers) {
1230
+ const name = resolveContainerName(c, props.containers);
1231
+ if (!name) {
1232
+ throw new Error("All containers must have a name when multiple containers are defined");
1233
+ }
1234
+ const containerImage = resolveImage(this, c.image, platform, name);
1235
+ const resolvedSecrets = resolveSecrets(this, c.ssm, c.secrets, `${name}`);
1236
+ let logGroupProps = {
1237
+ retention: resolveLogRetention(c.logging?.retention)
1238
+ };
1239
+ if (props.transform?.logGroup) {
1240
+ logGroupProps = props.transform.logGroup(logGroupProps);
1241
+ }
1242
+ const logGroup = new logs4.LogGroup(this, `${name}LogGroup`, logGroupProps);
1243
+ this.logGroups[name] = logGroup;
1244
+ let containerProps = {
1245
+ image: containerImage,
1246
+ logging: ecs3.LogDrivers.awsLogs({ streamPrefix: `${id}-${name}`, logGroup }),
1247
+ environment: c.environment,
1248
+ secrets: resolvedSecrets,
1249
+ command: c.command,
1250
+ entryPoint: c.entrypoint,
1251
+ portMappings: c.port ? [{ containerPort: c.port }] : void 0,
1252
+ cpu: c.cpu,
1253
+ memoryLimitMiB: c.memory,
1254
+ healthCheck: c.health ? {
1255
+ command: c.health.command,
1256
+ interval: c.health.interval ? Duration4.seconds(c.health.interval) : void 0,
1257
+ timeout: c.health.timeout ? Duration4.seconds(c.health.timeout) : void 0,
1258
+ retries: c.health.retries,
1259
+ startPeriod: c.health.startPeriod ? Duration4.seconds(c.health.startPeriod) : void 0
1260
+ } : void 0
1261
+ };
1262
+ if (props.transform?.container) {
1263
+ containerProps = props.transform.container(containerProps);
1264
+ }
1265
+ this.containers[name] = this.taskDefinition.addContainer(name, containerProps);
1266
+ }
1267
+ this.securityGroup = new ec22.SecurityGroup(this, "ServiceSecurityGroup", {
1268
+ vpc: props.cluster.vpc,
1269
+ description: `ECS Service SG for ${id}`,
1270
+ allowAllOutbound: true
1271
+ });
1272
+ if (props.loadBalancer) {
1273
+ const lb = props.loadBalancer;
1274
+ const isPublic = lb.public !== false;
1275
+ const rules = lb.rules ?? autoRules(props.containers, !!lb.domain);
1276
+ const resolvedRules = rules.map((rule) => ({
1277
+ ...rule,
1278
+ container: rule.container ? rule.container : resolveDefaultContainerName(props.containers)
1279
+ }));
1280
+ for (const rule of resolvedRules) {
1281
+ if (!rule.redirect && !this.containers[rule.container]) {
1282
+ throw new Error(`Routing rule references unknown container '${rule.container}'`);
1283
+ }
1284
+ }
1285
+ let albSgProps = {
1286
+ vpc: props.cluster.vpc,
1287
+ description: `ALB SG for ${id}`,
1288
+ allowAllOutbound: true
1289
+ };
1290
+ if (props.transform?.albSecurityGroup) {
1291
+ albSgProps = props.transform.albSecurityGroup(albSgProps);
1292
+ }
1293
+ const albSg = new ec22.SecurityGroup(this, "AlbSecurityGroup", albSgProps);
1294
+ let albProps = {
1295
+ vpc: props.cluster.vpc,
1296
+ internetFacing: isPublic,
1297
+ securityGroup: albSg
1298
+ };
1299
+ if (props.transform?.alb) {
1300
+ albProps = props.transform.alb(albProps);
1301
+ }
1302
+ this.alb = new elbv2.ApplicationLoadBalancer(this, "Alb", albProps);
1303
+ const tgMap = {};
1304
+ for (const rule of resolvedRules) {
1305
+ if (rule.redirect) continue;
1306
+ const forwardPort = resolveForwardPort(rule, props.containers);
1307
+ const tgKey = `${rule.container}:${forwardPort}`;
1308
+ if (tgMap[tgKey]) continue;
1309
+ const health = lb.health;
1310
+ let targetGroupProps = {
1311
+ vpc: props.cluster.vpc,
1312
+ port: forwardPort,
1313
+ protocol: elbv2.ApplicationProtocol.HTTP,
1314
+ targetType: elbv2.TargetType.IP,
1315
+ deregistrationDelay: Duration4.seconds(30),
1316
+ ...health ? {
1317
+ healthCheck: {
1318
+ path: health.path ?? "/health",
1319
+ interval: health.interval ? Duration4.seconds(health.interval) : void 0,
1320
+ timeout: health.timeout ? Duration4.seconds(health.timeout) : void 0,
1321
+ healthyThresholdCount: health.healthyThreshold,
1322
+ unhealthyThresholdCount: health.unhealthyThreshold,
1323
+ healthyHttpCodes: health.successCodes
1324
+ }
1325
+ } : {}
1326
+ };
1327
+ if (props.transform?.targetGroup) {
1328
+ targetGroupProps = props.transform.targetGroup(targetGroupProps);
1329
+ }
1330
+ tgMap[tgKey] = new elbv2.ApplicationTargetGroup(this, `${rule.container}Port${forwardPort}Tg`, targetGroupProps);
1331
+ }
1332
+ this.targetGroups = tgMap;
1333
+ const listenerGroups = /* @__PURE__ */ new Map();
1334
+ for (const rule of resolvedRules) {
1335
+ const { port, protocol } = parsePortProtocol(rule.listen);
1336
+ if (!listenerGroups.has(port)) {
1337
+ listenerGroups.set(port, { protocol, rules: [] });
1338
+ }
1339
+ listenerGroups.get(port).rules.push(rule);
1340
+ }
1341
+ const certificate = lb.domain ? acm2.Certificate.fromCertificateArn(this, "Certificate", lb.domain.certificateArn) : void 0;
1342
+ let primaryListener;
1343
+ for (const [listenPort, group] of listenerGroups) {
1344
+ const isHttps = group.protocol === "https";
1345
+ const defaultRedirect = group.rules.find((r) => r.redirect && !r.conditions);
1346
+ const defaultForward = group.rules.find((r) => !r.redirect && !r.conditions);
1347
+ const conditionalForwards = group.rules.filter((r) => !r.redirect && !!r.conditions);
1348
+ let listener;
1349
+ if (defaultRedirect) {
1350
+ const target = parsePortProtocol(defaultRedirect.redirect);
1351
+ let listenerProps = {
1352
+ port: listenPort,
1353
+ defaultAction: elbv2.ListenerAction.redirect({
1354
+ protocol: target.protocol.toUpperCase(),
1355
+ port: String(target.port),
1356
+ permanent: true
1357
+ })
1358
+ };
1359
+ if (props.transform?.listener) {
1360
+ listenerProps = props.transform.listener(listenerProps);
1361
+ }
1362
+ listener = this.alb.addListener(`Listener${listenPort}`, listenerProps);
1363
+ } else if (defaultForward) {
1364
+ const forwardPort = resolveForwardPort(defaultForward, props.containers);
1365
+ const tgKey = `${defaultForward.container}:${forwardPort}`;
1366
+ let listenerProps = {
1367
+ port: listenPort,
1368
+ ...isHttps && certificate ? { certificates: [certificate] } : {},
1369
+ defaultTargetGroups: [tgMap[tgKey]]
1370
+ };
1371
+ if (props.transform?.listener) {
1372
+ listenerProps = props.transform.listener(listenerProps);
1373
+ }
1374
+ listener = this.alb.addListener(`Listener${listenPort}`, listenerProps);
1375
+ } else {
1376
+ throw new Error(`Listener on port ${listenPort} has no default rule (a rule without conditions is required)`);
1377
+ }
1378
+ if (!primaryListener && !defaultRedirect) {
1379
+ primaryListener = listener;
1380
+ }
1381
+ let autoPriority = 1;
1382
+ for (const rule of conditionalForwards) {
1383
+ const forwardPort = resolveForwardPort(rule, props.containers);
1384
+ const tgKey = `${rule.container}:${forwardPort}`;
1385
+ const priority = autoPriority++;
1386
+ const conditions = [];
1387
+ if (rule.conditions.path) {
1388
+ conditions.push(elbv2.ListenerCondition.pathPatterns([rule.conditions.path]));
1389
+ }
1390
+ if (rule.conditions.header) {
1391
+ conditions.push(elbv2.ListenerCondition.httpHeader(rule.conditions.header.name, rule.conditions.header.values));
1392
+ }
1393
+ if (rule.conditions.query) {
1394
+ conditions.push(elbv2.ListenerCondition.queryStrings(rule.conditions.query));
1395
+ }
1396
+ new elbv2.ApplicationListenerRule(this, `Listener${listenPort}Rule${priority}`, {
1397
+ listener,
1398
+ priority,
1399
+ conditions,
1400
+ targetGroups: [tgMap[tgKey]]
1401
+ });
1402
+ }
1403
+ albSg.addIngressRule(ec22.Peer.anyIpv4(), ec22.Port.tcp(listenPort), `Port ${listenPort}`);
1404
+ }
1405
+ this.listener = primaryListener;
1406
+ const forwardPorts = /* @__PURE__ */ new Set();
1407
+ for (const rule of resolvedRules) {
1408
+ if (rule.redirect) continue;
1409
+ forwardPorts.add(resolveForwardPort(rule, props.containers));
1410
+ }
1411
+ for (const port of forwardPorts) {
1412
+ this.securityGroup.addIngressRule(albSg, ec22.Port.tcp(port), `From ALB on port ${port}`);
1413
+ }
1414
+ }
1415
+ const capacityProviderStrategies = resolveCapacity(props.capacity);
1416
+ const desiredCount = props.scaling?.min ?? 1;
1417
+ let serviceProps = {
1418
+ cluster: props.cluster,
1419
+ taskDefinition: this.taskDefinition,
1420
+ desiredCount,
1421
+ securityGroups: [this.securityGroup],
1422
+ vpcSubnets: { subnetType: ec22.SubnetType.PRIVATE_WITH_EGRESS },
1423
+ assignPublicIp: false,
1424
+ circuitBreaker: { rollback: true },
1425
+ ...capacityProviderStrategies ? { capacityProviderStrategies } : {}
1426
+ };
1427
+ if (props.transform?.service) {
1428
+ serviceProps = props.transform.service(serviceProps);
1429
+ }
1430
+ this.service = new ecs3.FargateService(this, "Service", serviceProps);
1431
+ if (this.targetGroups) {
1432
+ for (const tg of Object.values(this.targetGroups)) {
1433
+ this.service.attachToApplicationTargetGroup(tg);
1434
+ }
1435
+ }
1436
+ if (props.scaling) {
1437
+ let autoScalingProps = {
1438
+ minCapacity: props.scaling.min ?? 1,
1439
+ maxCapacity: props.scaling.max ?? 4
1440
+ };
1441
+ if (props.transform?.autoScalingTarget) {
1442
+ autoScalingProps = props.transform.autoScalingTarget(autoScalingProps);
1443
+ }
1444
+ const scaling = this.service.autoScaleTaskCount(autoScalingProps);
1445
+ scaling.scaleOnCpuUtilization("CpuScaling", {
1446
+ targetUtilizationPercent: props.scaling.cpuTarget ?? 70,
1447
+ scaleInCooldown: props.scaling.scaleInCooldown ? Duration4.seconds(props.scaling.scaleInCooldown) : void 0,
1448
+ scaleOutCooldown: props.scaling.scaleOutCooldown ? Duration4.seconds(props.scaling.scaleOutCooldown) : void 0
1449
+ });
1450
+ if (props.scaling.memoryTarget) {
1451
+ scaling.scaleOnMemoryUtilization("MemoryScaling", {
1452
+ targetUtilizationPercent: props.scaling.memoryTarget,
1453
+ scaleInCooldown: props.scaling.scaleInCooldown ? Duration4.seconds(props.scaling.scaleInCooldown) : void 0,
1454
+ scaleOutCooldown: props.scaling.scaleOutCooldown ? Duration4.seconds(props.scaling.scaleOutCooldown) : void 0
1455
+ });
1456
+ }
1457
+ if (props.scaling.requestsPerTarget && this.targetGroups) {
1458
+ const targetGroup = Object.values(this.targetGroups)[0];
1459
+ scaling.scaleOnRequestCount("RequestScaling", {
1460
+ requestsPerTarget: props.scaling.requestsPerTarget,
1461
+ targetGroup,
1462
+ scaleInCooldown: props.scaling.scaleInCooldown ? Duration4.seconds(props.scaling.scaleInCooldown) : void 0,
1463
+ scaleOutCooldown: props.scaling.scaleOutCooldown ? Duration4.seconds(props.scaling.scaleOutCooldown) : void 0
1464
+ });
1465
+ }
1466
+ }
1467
+ }
1468
+ };
1469
+ function resolveContainerName(c, containers) {
1470
+ return c.name ?? (containers.length === 1 ? "Container" : void 0);
1471
+ }
1472
+ function parsePortProtocol(value) {
1473
+ const parts = value.split("/");
1474
+ return { port: Number(parts[0]), protocol: parts[1] ?? "http" };
1475
+ }
1476
+ function resolveDefaultContainerName(containers) {
1477
+ if (containers.length === 1) return resolveContainerName(containers[0], containers);
1478
+ throw new Error("container is required in routing rules when multiple containers are defined");
1479
+ }
1480
+ function resolveForwardPort(rule, containers) {
1481
+ if (rule.forward !== void 0) return rule.forward;
1482
+ const container = containers.find((c) => {
1483
+ return resolveContainerName(c, containers) === rule.container;
1484
+ });
1485
+ if (!container?.port) {
1486
+ throw new Error(`Container '${rule.container}' has no port and no forward specified in rule`);
1487
+ }
1488
+ return container.port;
1489
+ }
1490
+ function autoRules(containers, hasDomain) {
1491
+ const withPorts = containers.filter((c) => c.port);
1492
+ if (withPorts.length === 0) {
1493
+ throw new Error("No container with a port found for load balancer");
1494
+ }
1495
+ if (withPorts.length > 1) {
1496
+ throw new Error(
1497
+ "Multiple containers have ports but no routing rules are defined. Add loadBalancer.rules to specify routing."
1498
+ );
1499
+ }
1500
+ const name = resolveContainerName(withPorts[0], containers);
1501
+ if (hasDomain) {
1502
+ return [
1503
+ { listen: "443/https", container: name },
1504
+ { listen: "80/http", redirect: "443/https" }
1505
+ ];
1506
+ }
1507
+ return [{ listen: "80/http", container: name }];
1508
+ }
1509
+ function resolveImage(scope, image, platform, id) {
1510
+ if (image === void 0) {
1511
+ return ecs3.ContainerImage.fromAsset(".", { platform });
1512
+ }
1513
+ if (typeof image === "string") {
1514
+ return ecs3.ContainerImage.fromRegistry(image);
1515
+ }
1516
+ if (image instanceof ecrAssets.DockerImageAsset) {
1517
+ return ecs3.ContainerImage.fromDockerImageAsset(image);
1518
+ }
1519
+ return ecs3.ContainerImage.fromAsset(image.context ?? ".", {
1520
+ platform,
1521
+ file: image.dockerfile,
1522
+ buildArgs: image.args,
1523
+ target: image.target,
1524
+ exclude: image.exclude
1525
+ });
1526
+ }
1527
+ function resolveCapacity(capacity) {
1528
+ if (!capacity) return void 0;
1529
+ if (capacity === "spot") {
1530
+ return [{ capacityProvider: "FARGATE_SPOT", weight: 1 }];
1531
+ }
1532
+ const strategies = [];
1533
+ if (capacity.fargate) {
1534
+ strategies.push({
1535
+ capacityProvider: "FARGATE",
1536
+ weight: capacity.fargate.weight,
1537
+ base: capacity.fargate.base
1538
+ });
1539
+ }
1540
+ if (capacity.spot) {
1541
+ strategies.push({
1542
+ capacityProvider: "FARGATE_SPOT",
1543
+ weight: capacity.spot.weight,
1544
+ base: capacity.spot.base
1545
+ });
1546
+ }
1547
+ return strategies.length > 0 ? strategies : void 0;
1548
+ }
1549
+
1550
+ // src/helpers/ssr-site-ecs-service.ts
1551
+ function createSsrSiteEcsService(scope, props) {
1552
+ const assetBehaviors = props.assets?.behaviors ?? [];
1553
+ let bucketProps = {
1554
+ blockPublicAccess: s35.BlockPublicAccess.BLOCK_ALL,
1555
+ removalPolicy: RemovalPolicy4.RETAIN
1556
+ };
1557
+ if (props.transform?.bucket) {
1558
+ bucketProps = props.transform.bucket(bucketProps);
1559
+ }
1560
+ const bucket = new s35.Bucket(scope, "Bucket", bucketProps);
1561
+ const ecsService = new EcsFargateService(scope, "Ecs", {
1562
+ cluster: props.cluster,
1563
+ architecture: props.architecture,
1564
+ cpu: props.cpu,
1565
+ memoryLimitMiB: props.memoryLimitMiB,
1566
+ capacity: props.capacity,
1567
+ containers: [
1568
+ {
1569
+ image: props.image,
1570
+ port: props.port ?? 3e3,
1571
+ command: props.command,
1572
+ entrypoint: props.entrypoint,
1573
+ environment: props.environment,
1574
+ ssm: props.ssm,
1575
+ secrets: props.secrets
1576
+ }
1577
+ ],
1578
+ loadBalancer: {
1579
+ public: true,
1580
+ health: props.healthCheck
1581
+ },
1582
+ scaling: props.scaling,
1583
+ permissions: props.permissions,
1584
+ transform: {
1585
+ executionRole: props.transform?.executionRole,
1586
+ taskRole: props.transform?.taskRole,
1587
+ taskDefinition: props.transform?.taskDefinition,
1588
+ container: props.transform?.container,
1589
+ service: props.transform?.service,
1590
+ autoScalingTarget: props.transform?.autoScalingTarget,
1591
+ alb: props.transform?.alb,
1592
+ albSecurityGroup: props.transform?.albSecurityGroup,
1593
+ listener: props.transform?.listener,
1594
+ targetGroup: props.transform?.targetGroup,
1595
+ logGroup: props.transform?.logGroup
1596
+ }
1597
+ });
1598
+ const alb = ecsService.alb;
1599
+ const cfFunction = props.edge?.viewerRequest ?? createViewerRequestFunction(scope, "ViewerRequestFn");
1600
+ const defaultFunctionAssociations = [
1601
+ { function: cfFunction, eventType: cloudfront4.FunctionEventType.VIEWER_REQUEST }
1602
+ ];
1603
+ if (props.edge?.viewerResponse) {
1604
+ defaultFunctionAssociations.push({
1605
+ function: props.edge.viewerResponse,
1606
+ eventType: cloudfront4.FunctionEventType.VIEWER_RESPONSE
1607
+ });
1608
+ }
1609
+ const { certificate, domainNames } = resolveCertificate(scope, props.domain);
1610
+ const s3Origin = origins3.S3BucketOrigin.withOriginAccessControl(bucket);
1611
+ const albOrigin = new origins3.HttpOrigin(alb.loadBalancerDnsName, {
1612
+ protocolPolicy: cloudfront4.OriginProtocolPolicy.HTTP_ONLY
1613
+ });
1614
+ const additionalBehaviors = resolveAdditionalBehaviors(s3Origin, assetBehaviors);
1615
+ let distributionProps = {
1616
+ defaultBehavior: {
1617
+ origin: albOrigin,
1618
+ viewerProtocolPolicy: cloudfront4.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
1619
+ cachePolicy: cloudfront4.CachePolicy.CACHING_DISABLED,
1620
+ originRequestPolicy: cloudfront4.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
1621
+ allowedMethods: cloudfront4.AllowedMethods.ALLOW_ALL,
1622
+ functionAssociations: defaultFunctionAssociations
1623
+ },
1624
+ additionalBehaviors,
1625
+ domainNames,
1626
+ certificate
1627
+ };
1628
+ if (props.transform?.distribution) {
1629
+ distributionProps = props.transform.distribution(distributionProps);
1630
+ }
1631
+ const distribution = new cloudfront4.Distribution(scope, "Distribution", distributionProps);
1632
+ bucket.grantReadWrite(ecsService.taskDefinition.taskRole);
1633
+ ecsService.taskDefinition.taskRole.addToPrincipalPolicy(
1634
+ new iam6.PolicyStatement({
1635
+ actions: ["cloudfront:CreateInvalidation"],
1636
+ resources: [`arn:aws:cloudfront::*:distribution/${distribution.distributionId}`]
1637
+ })
1638
+ );
1639
+ ecsService.containers["Container"].addEnvironment("CLOUDFRONT_DISTRIBUTION_ID", distribution.distributionId);
1640
+ ecsService.containers["Container"].addEnvironment("S3_BUCKET_NAME", bucket.bucketName);
1641
+ if (props.assets?.path) {
1642
+ const fileOptions = props.assets.fileOptions ?? [{ files: "**", cacheControl: "max-age=31536000,public,immutable" }];
1643
+ resolveAssetDeployment(scope, "", {
1644
+ path: props.assets.path,
1645
+ bucket,
1646
+ prefix: props.assets.prefix,
1647
+ fileOptions,
1648
+ distribution,
1649
+ invalidation: props.invalidation
1650
+ });
1651
+ }
1652
+ return { bucket, distribution, ecsService };
1653
+ }
1654
+
1655
+ // src/react-router-ssr-site-ecs-fargate-service.ts
1656
+ var ReactRouterSsrSiteEcsFargateService = class extends Construct12 {
1657
+ bucket;
1658
+ distribution;
1659
+ ecsService;
1660
+ constructor(scope, id, props) {
1661
+ super(scope, id);
1662
+ const buildDir = props.buildDirectory ?? "build";
1663
+ const appRoot = !props.path || props.path === "." ? "." : props.path;
1664
+ const basePath = appRoot === "." ? buildDir : `${appRoot}/${buildDir}`;
1665
+ const port = props.port ?? 3e3;
1666
+ let image = props.image;
1667
+ if (!image) {
1668
+ image = resolveReactRouterDockerContext({
1669
+ appRoot,
1670
+ buildDir,
1671
+ basePath,
1672
+ port,
1673
+ className: "ReactRouterSsrSiteEcsFargateService"
1674
+ });
1675
+ }
1676
+ const merged = {
1677
+ cluster: props.cluster,
1678
+ image,
1679
+ port,
1680
+ command: props.command,
1681
+ entrypoint: props.entrypoint,
1682
+ environment: props.environment,
1683
+ ssm: props.ssm,
1684
+ secrets: props.secrets,
1685
+ architecture: props.architecture,
1686
+ cpu: props.cpu,
1687
+ memoryLimitMiB: props.memoryLimitMiB,
1688
+ capacity: props.capacity,
1689
+ healthCheck: props.healthCheck,
1690
+ scaling: props.scaling,
1691
+ permissions: props.permissions,
1692
+ domain: props.domain,
1693
+ edge: props.edge,
1694
+ invalidation: props.invalidation,
1695
+ transform: props.transform,
1696
+ assets: {
1697
+ ...props.assets,
1698
+ path: props.assets?.path ?? `${basePath}/client`,
1699
+ behaviors: props.assets?.behaviors ?? [{ pathPattern: "/assets/*", cachePolicy: "CACHING_OPTIMIZED" }],
1700
+ fileOptions: props.assets?.fileOptions ?? [{ files: "**", cacheControl: "max-age=31536000,public,immutable" }]
1701
+ }
1702
+ };
1703
+ const resources = createSsrSiteEcsService(this, merged);
1704
+ this.bucket = resources.bucket;
1705
+ this.distribution = resources.distribution;
1706
+ this.ecsService = resources.ecsService;
1707
+ }
1708
+ };
1709
+
1710
+ // src/react-router-ecr-image.ts
1711
+ import { Construct as Construct13 } from "constructs";
1712
+ import * as ecrAssets2 from "aws-cdk-lib/aws-ecr-assets";
1713
+ var ReactRouterEcrImage = class extends Construct13 {
1714
+ asset;
1715
+ constructor(scope, id, props = {}) {
1716
+ super(scope, id);
1717
+ const buildDir = props.buildDirectory ?? "build";
1718
+ const appRoot = !props.path || props.path === "." ? "." : props.path;
1719
+ const basePath = appRoot === "." ? buildDir : `${appRoot}/${buildDir}`;
1720
+ const port = props.port ?? 3e3;
1721
+ const arch = props.architecture ?? "X86_64";
1722
+ const platform = props.platform ?? (arch === "ARM64" ? ecrAssets2.Platform.LINUX_ARM64 : ecrAssets2.Platform.LINUX_AMD64);
1723
+ let context;
1724
+ let dockerfile;
1725
+ if (props.image) {
1726
+ context = props.image.context ?? appRoot;
1727
+ dockerfile = props.image.dockerfile;
1728
+ } else {
1729
+ const resolved = resolveReactRouterDockerContext({
1730
+ appRoot,
1731
+ buildDir,
1732
+ basePath,
1733
+ port,
1734
+ className: "ReactRouterEcrImage"
1735
+ });
1736
+ context = resolved.context;
1737
+ dockerfile = resolved.dockerfile;
1738
+ }
1739
+ this.asset = new ecrAssets2.DockerImageAsset(this, "Image", {
1740
+ directory: context,
1741
+ file: dockerfile,
1742
+ platform,
1743
+ buildArgs: props.image?.args,
1744
+ target: props.image?.target,
1745
+ exclude: props.image?.exclude
1746
+ });
1747
+ }
1748
+ };
1749
+
1750
+ // src/mysql-database.ts
1751
+ import { Construct as Construct14 } from "constructs";
1752
+ import { Duration as Duration5, RemovalPolicy as RemovalPolicy5 } from "aws-cdk-lib";
1753
+ import * as ec23 from "aws-cdk-lib/aws-ec2";
1754
+ import * as rds from "aws-cdk-lib/aws-rds";
1755
+ var MySqlDatabase = class extends Construct14 {
1756
+ instance;
1757
+ securityGroup;
1758
+ secret;
1759
+ connections;
1760
+ rdsProxy;
1761
+ get host() {
1762
+ return this.instance.dbInstanceEndpointAddress;
1763
+ }
1764
+ get port() {
1765
+ return this.instance.dbInstanceEndpointPort;
1766
+ }
1767
+ get proxyEndpoint() {
1768
+ return this.rdsProxy?.endpoint;
1769
+ }
1770
+ constructor(scope, id, props) {
1771
+ super(scope, id);
1772
+ const username = props.username ?? "root";
1773
+ let instanceProps = {
1774
+ engine: rds.DatabaseInstanceEngine.mysql({
1775
+ version: props.version ?? rds.MysqlEngineVersion.VER_8_0
1776
+ }),
1777
+ instanceType: props.instance ?? ec23.InstanceType.of(ec23.InstanceClass.T3, ec23.InstanceSize.MICRO),
1778
+ vpc: props.vpc,
1779
+ vpcSubnets: { subnetType: resolvePrivateSubnetType(props.vpc) },
1780
+ databaseName: props.database,
1781
+ credentials: rds.Credentials.fromGeneratedSecret(username),
1782
+ maxAllocatedStorage: props.storage ? Math.max(props.storage, 5e3) : 5e3,
1783
+ allocatedStorage: props.storage ?? 20,
1784
+ multiAz: props.multiAz ?? false,
1785
+ backupRetention: Duration5.days(props.backupRetentionDays ?? 7),
1786
+ deletionProtection: props.deletionProtection ?? true,
1787
+ removalPolicy: RemovalPolicy5.SNAPSHOT,
1788
+ storageEncrypted: true,
1789
+ enablePerformanceInsights: props.performanceInsights ?? false,
1790
+ port: 3306
1791
+ };
1792
+ if (props.transform?.instance) {
1793
+ instanceProps = props.transform.instance(instanceProps);
1794
+ }
1795
+ this.instance = new rds.DatabaseInstance(this, "Instance", instanceProps);
1796
+ this.securityGroup = this.instance.connections.securityGroups[0];
1797
+ this.secret = this.instance.secret;
1798
+ this.connections = this.instance.connections;
1799
+ if (props.proxy) {
1800
+ let proxyProps = {
1801
+ proxyTarget: rds.ProxyTarget.fromInstance(this.instance),
1802
+ secrets: [this.secret],
1803
+ vpc: props.vpc,
1804
+ securityGroups: [this.securityGroup],
1805
+ requireTLS: false
1806
+ };
1807
+ if (props.transform?.proxy) {
1808
+ proxyProps = props.transform.proxy(proxyProps);
1809
+ }
1810
+ this.rdsProxy = new rds.DatabaseProxy(this, "Proxy", proxyProps);
1811
+ }
1812
+ }
1813
+ };
1814
+ function resolvePrivateSubnetType(vpc) {
1815
+ if (vpc instanceof ec23.Vpc && vpc.isolatedSubnets.length > 0) return ec23.SubnetType.PRIVATE_ISOLATED;
1816
+ return ec23.SubnetType.PRIVATE_WITH_EGRESS;
1817
+ }
1818
+
1819
+ // src/aurora-database.ts
1820
+ import { Construct as Construct15 } from "constructs";
1821
+ import { Duration as Duration6, RemovalPolicy as RemovalPolicy6 } from "aws-cdk-lib";
1822
+ import * as ec24 from "aws-cdk-lib/aws-ec2";
1823
+ import * as rds2 from "aws-cdk-lib/aws-rds";
1824
+ var AuroraDatabase = class extends Construct15 {
1825
+ cluster;
1826
+ securityGroup;
1827
+ secret;
1828
+ connections;
1829
+ rdsProxy;
1830
+ get host() {
1831
+ return this.cluster.clusterEndpoint.hostname;
1832
+ }
1833
+ get port() {
1834
+ return this.cluster.clusterEndpoint.port.toString();
1835
+ }
1836
+ get readerHost() {
1837
+ return this.cluster.clusterReadEndpoint.hostname;
1838
+ }
1839
+ get proxyEndpoint() {
1840
+ return this.rdsProxy?.endpoint;
1841
+ }
1842
+ constructor(scope, id, props) {
1843
+ super(scope, id);
1844
+ const engine = resolveEngine(props.engine, props.version);
1845
+ const username = props.username ?? (props.engine === "postgres" ? "postgres" : "root");
1846
+ const readers = Array.from(
1847
+ { length: props.readers ?? 0 },
1848
+ (_, i) => rds2.ClusterInstance.serverlessV2(`Reader${i + 1}`, { scaleWithWriter: true })
1849
+ );
1850
+ let clusterProps = {
1851
+ engine,
1852
+ port: props.port,
1853
+ writer: rds2.ClusterInstance.serverlessV2("Writer"),
1854
+ readers,
1855
+ serverlessV2MinCapacity: props.scaling?.min ?? 0,
1856
+ serverlessV2MaxCapacity: props.scaling?.max ?? 4,
1857
+ serverlessV2AutoPauseDuration: props.scaling?.pauseAfter ? parseDuration(props.scaling.pauseAfter) : void 0,
1858
+ vpc: props.vpc,
1859
+ vpcSubnets: { subnetType: resolvePrivateSubnetType2(props.vpc) },
1860
+ defaultDatabaseName: props.database,
1861
+ credentials: rds2.Credentials.fromGeneratedSecret(username),
1862
+ storageEncrypted: true,
1863
+ deletionProtection: props.deletionProtection ?? true,
1864
+ removalPolicy: RemovalPolicy6.SNAPSHOT,
1865
+ backup: { retention: Duration6.days(props.backupRetentionDays ?? 7) },
1866
+ enableDataApi: props.dataApi ?? false,
1867
+ enablePerformanceInsights: props.performanceInsights ?? false
1868
+ };
1869
+ if (props.transform?.cluster) {
1870
+ clusterProps = props.transform.cluster(clusterProps);
1871
+ }
1872
+ this.cluster = new rds2.DatabaseCluster(this, "Cluster", clusterProps);
1873
+ if (!this.cluster.secret) {
1874
+ throw new Error("AuroraDatabase: cluster secret is missing \u2014 ensure credentials are not overridden via transform");
1875
+ }
1876
+ this.securityGroup = this.cluster.connections.securityGroups[0];
1877
+ this.secret = this.cluster.secret;
1878
+ this.connections = this.cluster.connections;
1879
+ if (props.proxy) {
1880
+ let proxyProps = {
1881
+ proxyTarget: rds2.ProxyTarget.fromCluster(this.cluster),
1882
+ secrets: [this.secret],
1883
+ vpc: props.vpc,
1884
+ securityGroups: [this.securityGroup],
1885
+ requireTLS: false
1886
+ };
1887
+ if (props.transform?.proxy) {
1888
+ proxyProps = props.transform.proxy(proxyProps);
1889
+ }
1890
+ this.rdsProxy = new rds2.DatabaseProxy(this, "Proxy", proxyProps);
1891
+ }
1892
+ }
1893
+ };
1894
+ function parseDuration(value) {
1895
+ const match = value.match(/^(\d+)\s+(second|seconds|minute|minutes|hour|hours)$/);
1896
+ if (!match) throw new Error(`Invalid duration: ${value}`);
1897
+ const amount = Number(match[1]);
1898
+ const unit = match[2];
1899
+ if (unit.startsWith("hour")) return Duration6.hours(amount);
1900
+ if (unit.startsWith("minute")) return Duration6.minutes(amount);
1901
+ return Duration6.seconds(amount);
1902
+ }
1903
+ function resolvePrivateSubnetType2(vpc) {
1904
+ if (vpc instanceof ec24.Vpc && vpc.isolatedSubnets.length > 0) return ec24.SubnetType.PRIVATE_ISOLATED;
1905
+ return ec24.SubnetType.PRIVATE_WITH_EGRESS;
1906
+ }
1907
+ function resolveEngine(engine, version) {
1908
+ if (engine === "postgres") {
1909
+ return rds2.DatabaseClusterEngine.auroraPostgres({
1910
+ version
1911
+ });
1912
+ }
1913
+ return rds2.DatabaseClusterEngine.auroraMysql({
1914
+ version
1915
+ });
1916
+ }
1917
+
1918
+ // src/redis.ts
1919
+ import { Construct as Construct16 } from "constructs";
1920
+ import * as ec25 from "aws-cdk-lib/aws-ec2";
1921
+ import * as elasticache from "aws-cdk-lib/aws-elasticache";
1922
+ import * as secretsmanager2 from "aws-cdk-lib/aws-secretsmanager";
1923
+ var Redis = class extends Construct16 {
1924
+ replicationGroup;
1925
+ securityGroup;
1926
+ subnetGroup;
1927
+ parameterGroup;
1928
+ secret;
1929
+ get host() {
1930
+ return this.replicationGroup.attrPrimaryEndPointAddress;
1931
+ }
1932
+ get port() {
1933
+ return this.replicationGroup.attrPrimaryEndPointPort;
1934
+ }
1935
+ constructor(scope, id, props) {
1936
+ super(scope, id);
1937
+ const engine = props.engine ?? "redis";
1938
+ const version = resolveVersion(engine, props.version);
1939
+ const replicas = props.replicas ?? 0;
1940
+ this.securityGroup = new ec25.SecurityGroup(this, "SecurityGroup", {
1941
+ vpc: props.vpc,
1942
+ description: `Security group for ${id} ElastiCache`
1943
+ });
1944
+ this.securityGroup.addIngressRule(ec25.Peer.ipv4(props.vpc.vpcCidrBlock), ec25.Port.tcp(6379), "Allow Redis access from VPC");
1945
+ const subnetType = resolvePrivateSubnetType3(props.vpc);
1946
+ const subnets = props.vpc.selectSubnets({ subnetType });
1947
+ let subnetGroupProps = {
1948
+ description: `Subnet group for ${id}`,
1949
+ subnetIds: subnets.subnetIds
1950
+ };
1951
+ if (props.transform?.subnetGroup) {
1952
+ subnetGroupProps = props.transform.subnetGroup(subnetGroupProps);
1953
+ }
1954
+ this.subnetGroup = new elasticache.CfnSubnetGroup(this, "SubnetGroup", subnetGroupProps);
1955
+ if (props.parameters) {
1956
+ let parameterGroupProps = {
1957
+ cacheParameterGroupFamily: resolveParameterGroupFamily(engine, version),
1958
+ description: `Parameter group for ${id}`,
1959
+ properties: props.parameters
1960
+ };
1961
+ if (props.transform?.parameterGroup) {
1962
+ parameterGroupProps = props.transform.parameterGroup(parameterGroupProps);
1963
+ }
1964
+ this.parameterGroup = new elasticache.CfnParameterGroup(this, "ParameterGroup", parameterGroupProps);
1965
+ }
1966
+ this.secret = new secretsmanager2.Secret(this, "AuthToken", {
1967
+ description: `Auth token for ${id} ElastiCache`,
1968
+ generateSecretString: {
1969
+ excludePunctuation: true,
1970
+ passwordLength: 64
1971
+ }
1972
+ });
1973
+ let replicationGroupProps = {
1974
+ replicationGroupDescription: `${id} replication group`,
1975
+ engine,
1976
+ engineVersion: version,
1977
+ cacheNodeType: props.instance ?? "cache.t4g.micro",
1978
+ numCacheClusters: replicas + 1,
1979
+ port: 6379,
1980
+ automaticFailoverEnabled: replicas >= 1,
1981
+ multiAzEnabled: false,
1982
+ cacheSubnetGroupName: this.subnetGroup.ref,
1983
+ cacheParameterGroupName: this.parameterGroup?.ref,
1984
+ securityGroupIds: [this.securityGroup.securityGroupId],
1985
+ transitEncryptionEnabled: true,
1986
+ atRestEncryptionEnabled: true,
1987
+ authToken: this.secret.secretValue.unsafeUnwrap()
1988
+ };
1989
+ if (props.transform?.replicationGroup) {
1990
+ replicationGroupProps = props.transform.replicationGroup(replicationGroupProps);
1991
+ }
1992
+ this.replicationGroup = new elasticache.CfnReplicationGroup(this, "ReplicationGroup", replicationGroupProps);
1993
+ }
1994
+ };
1995
+ function resolvePrivateSubnetType3(vpc) {
1996
+ if (vpc instanceof ec25.Vpc && vpc.isolatedSubnets.length > 0) return ec25.SubnetType.PRIVATE_ISOLATED;
1997
+ return ec25.SubnetType.PRIVATE_WITH_EGRESS;
1998
+ }
1999
+ function resolveVersion(engine, version) {
2000
+ if (version) return version;
2001
+ return engine === "valkey" ? "7.2" : "7.1";
2002
+ }
2003
+ function resolveParameterGroupFamily(engine, version) {
2004
+ const major = version.split(".")[0];
2005
+ if (engine === "valkey") return `valkey${major}`;
2006
+ return `redis${major}.x`;
2007
+ }
2008
+
2009
+ // src/email.ts
2010
+ import { Construct as Construct17 } from "constructs";
2011
+ import * as ses from "aws-cdk-lib/aws-ses";
2012
+ import * as route53 from "aws-cdk-lib/aws-route53";
2013
+ var Email = class extends Construct17 {
2014
+ identity;
2015
+ configurationSet;
2016
+ dmarcRecord;
2017
+ _sender;
2018
+ get sender() {
2019
+ return this._sender;
2020
+ }
2021
+ get configSetName() {
2022
+ return this.configurationSet.configurationSetName;
2023
+ }
2024
+ constructor(scope, id, props) {
2025
+ super(scope, id);
2026
+ this._sender = props.sender;
2027
+ const senderType = resolveSenderType(props.sender);
2028
+ if (senderType === "email" && props.dmarc !== void 0) {
2029
+ throw new Error("DMARC records can only be configured for domain senders, not email addresses");
2030
+ }
2031
+ let configurationSetProps = {
2032
+ reputationMetrics: true
2033
+ };
2034
+ if (props.transform?.configurationSet) {
2035
+ configurationSetProps = props.transform.configurationSet(configurationSetProps);
2036
+ }
2037
+ this.configurationSet = new ses.ConfigurationSet(this, "ConfigurationSet", configurationSetProps);
2038
+ const identity = resolveIdentity(senderType, props.sender, props.hostedZone);
2039
+ let identityProps = {
2040
+ identity,
2041
+ configurationSet: this.configurationSet
2042
+ };
2043
+ if (props.transform?.identity) {
2044
+ identityProps = props.transform.identity(identityProps);
2045
+ }
2046
+ this.identity = new ses.EmailIdentity(this, "Identity", identityProps);
2047
+ if (senderType === "domain" && props.hostedZone) {
2048
+ const dmarcValue = props.dmarc ?? "v=DMARC1; p=none;";
2049
+ this.dmarcRecord = new route53.TxtRecord(this, "DmarcRecord", {
2050
+ zone: props.hostedZone,
2051
+ recordName: `_dmarc.${props.sender}`,
2052
+ values: [dmarcValue]
2053
+ });
2054
+ }
2055
+ if (props.events) {
2056
+ for (const [i, event] of props.events.entries()) {
2057
+ const types = event.types ? event.types.map(resolveEmailSendingEvent) : void 0;
2058
+ const destination = resolveEventDestination(event);
2059
+ this.configurationSet.addEventDestination(`EventDestination${i}`, {
2060
+ destination,
2061
+ enabled: event.enabled,
2062
+ events: types
2063
+ });
2064
+ }
2065
+ }
2066
+ }
2067
+ };
2068
+ function resolveSenderType(sender) {
2069
+ return sender.includes("@") ? "email" : "domain";
2070
+ }
2071
+ function resolveIdentity(senderType, sender, hostedZone) {
2072
+ if (senderType === "email") return ses.Identity.email(sender);
2073
+ if (hostedZone) return ses.Identity.publicHostedZone(hostedZone);
2074
+ return ses.Identity.domain(sender);
2075
+ }
2076
+ function resolveEventDestination(event) {
2077
+ if (event.snsTopic) return ses.EventDestination.snsTopic(event.snsTopic);
2078
+ if (event.cloudWatchDimensions) {
2079
+ return ses.EventDestination.cloudWatchDimensions(
2080
+ event.cloudWatchDimensions.map((d) => ({
2081
+ name: d.name,
2082
+ source: d.source,
2083
+ defaultValue: d.defaultValue
2084
+ }))
2085
+ );
2086
+ }
2087
+ throw new Error('Event destination must specify either "snsTopic" or "cloudWatchDimensions"');
2088
+ }
2089
+ function resolveEmailSendingEvent(type) {
2090
+ switch (type) {
2091
+ case "send":
2092
+ return ses.EmailSendingEvent.SEND;
2093
+ case "reject":
2094
+ return ses.EmailSendingEvent.REJECT;
2095
+ case "bounce":
2096
+ return ses.EmailSendingEvent.BOUNCE;
2097
+ case "complaint":
2098
+ return ses.EmailSendingEvent.COMPLAINT;
2099
+ case "delivery":
2100
+ return ses.EmailSendingEvent.DELIVERY;
2101
+ case "open":
2102
+ return ses.EmailSendingEvent.OPEN;
2103
+ case "click":
2104
+ return ses.EmailSendingEvent.CLICK;
2105
+ case "rendering-failure":
2106
+ return ses.EmailSendingEvent.RENDERING_FAILURE;
2107
+ case "delivery-delay":
2108
+ return ses.EmailSendingEvent.DELIVERY_DELAY;
2109
+ case "subscription":
2110
+ return ses.EmailSendingEvent.SUBSCRIPTION;
2111
+ }
2112
+ }
2113
+
2114
+ // src/sns-topic.ts
2115
+ import { Construct as Construct18 } from "constructs";
2116
+ import * as sns from "aws-cdk-lib/aws-sns";
2117
+ import * as snsSubscriptions from "aws-cdk-lib/aws-sns-subscriptions";
2118
+ var SnsTopic = class extends Construct18 {
2119
+ topic;
2120
+ get arn() {
2121
+ return this.topic.topicArn;
2122
+ }
2123
+ get topicName() {
2124
+ return this.topic.topicName;
2125
+ }
2126
+ constructor(scope, id, props = {}) {
2127
+ super(scope, id);
2128
+ let topicProps = {
2129
+ displayName: props.displayName,
2130
+ fifo: props.fifo
2131
+ };
2132
+ if (props.transform?.topic) {
2133
+ topicProps = props.transform.topic(topicProps);
2134
+ }
2135
+ this.topic = new sns.Topic(this, "Topic", topicProps);
2136
+ }
2137
+ /**
2138
+ * Subscribe a Lambda function to this topic.
2139
+ */
2140
+ subscribe(name, fn, args) {
2141
+ let subscriptionProps = {
2142
+ filterPolicy: args?.filterPolicy,
2143
+ filterPolicyWithMessageBody: args?.filterPolicyWithMessageBody,
2144
+ deadLetterQueue: args?.deadLetterQueue
2145
+ };
2146
+ if (args?.transform?.subscription) {
2147
+ subscriptionProps = args.transform.subscription(subscriptionProps);
2148
+ }
2149
+ this.topic.addSubscription(new snsSubscriptions.LambdaSubscription(fn, subscriptionProps));
2150
+ }
2151
+ /**
2152
+ * Subscribe an SQS queue to this topic.
2153
+ */
2154
+ subscribeQueue(name, queue, args) {
2155
+ let subscriptionProps = {
2156
+ filterPolicy: args?.filterPolicy,
2157
+ filterPolicyWithMessageBody: args?.filterPolicyWithMessageBody,
2158
+ deadLetterQueue: args?.deadLetterQueue,
2159
+ rawMessageDelivery: args?.rawMessageDelivery
2160
+ };
2161
+ if (args?.transform?.subscription) {
2162
+ subscriptionProps = args.transform.subscription(subscriptionProps);
2163
+ }
2164
+ this.topic.addSubscription(new snsSubscriptions.SqsSubscription(queue, subscriptionProps));
2165
+ }
2166
+ };
2167
+
2168
+ // src/event-bus.ts
2169
+ import { Construct as Construct19 } from "constructs";
2170
+ import * as events from "aws-cdk-lib/aws-events";
2171
+ import * as targets from "aws-cdk-lib/aws-events-targets";
2172
+ var EventBus2 = class extends Construct19 {
2173
+ bus;
2174
+ get arn() {
2175
+ return this.bus.eventBusArn;
2176
+ }
2177
+ get busName() {
2178
+ return this.bus.eventBusName;
2179
+ }
2180
+ constructor(scope, id, props = {}) {
2181
+ super(scope, id);
2182
+ let busProps = {
2183
+ eventBusName: props.eventBusName,
2184
+ description: props.description
2185
+ };
2186
+ if (props.transform?.bus) {
2187
+ busProps = props.transform.bus(busProps);
2188
+ }
2189
+ this.bus = new events.EventBus(this, "Bus", busProps);
2190
+ }
2191
+ /**
2192
+ * Subscribe a Lambda function to events on this bus.
2193
+ */
2194
+ subscribe(name, fn, args) {
2195
+ let ruleProps = {
2196
+ eventBus: this.bus,
2197
+ eventPattern: buildEventPattern(args?.pattern)
2198
+ };
2199
+ if (args?.transform?.rule) {
2200
+ ruleProps = args.transform.rule(ruleProps);
2201
+ }
2202
+ const rule = new events.Rule(this, `${name}Rule`, ruleProps);
2203
+ let targetProps = {};
2204
+ if (args?.transform?.target) {
2205
+ targetProps = args.transform.target(targetProps);
2206
+ }
2207
+ rule.addTarget(new targets.LambdaFunction(fn, targetProps));
2208
+ }
2209
+ /**
2210
+ * Subscribe an SQS queue to events on this bus.
2211
+ */
2212
+ subscribeQueue(name, queue, args) {
2213
+ let ruleProps = {
2214
+ eventBus: this.bus,
2215
+ eventPattern: buildEventPattern(args?.pattern)
2216
+ };
2217
+ if (args?.transform?.rule) {
2218
+ ruleProps = args.transform.rule(ruleProps);
2219
+ }
2220
+ const rule = new events.Rule(this, `${name}Rule`, ruleProps);
2221
+ let targetProps = {};
2222
+ if (args?.transform?.target) {
2223
+ targetProps = args.transform.target(targetProps);
2224
+ }
2225
+ rule.addTarget(new targets.SqsQueue(queue, targetProps));
2226
+ }
2227
+ };
2228
+ function buildEventPattern(pattern) {
2229
+ if (!pattern) return void 0;
2230
+ return {
2231
+ source: pattern.source,
2232
+ detailType: pattern.detailType,
2233
+ detail: pattern.detail
2234
+ };
2235
+ }
2236
+ export {
2237
+ AuroraDatabase,
2238
+ Bucket2 as Bucket,
2239
+ Cluster2 as Cluster,
2240
+ EcsFargateService,
2241
+ Email,
2242
+ EventBus2 as EventBus,
2243
+ LambdaFunction,
2244
+ MySqlDatabase,
2245
+ ReactRouterEcrImage,
2246
+ ReactRouterSsrSiteEcsFargateService,
2247
+ ReactRouterSsrSiteLambda,
2248
+ Redis,
2249
+ SnsTopic,
2250
+ StaticSite,
2251
+ Vpc2 as Vpc
2252
+ };