@stacksjs/cloud 0.59.11 → 0.61.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.
Files changed (48) hide show
  1. package/dist/index.js +843 -754
  2. package/package.json +30 -30
  3. package/src/cloud/ai.ts +5 -12
  4. package/src/cloud/aws-sdk-layer/nodejs/package-lock.json +7 -6
  5. package/src/cloud/aws-sdk-layer/nodejs/package.json +2 -2
  6. package/src/cloud/cdn.ts +41 -20
  7. package/src/cloud/cli.ts +1 -3
  8. package/src/cloud/compute.ts +39 -17
  9. package/src/cloud/dashboard.ts +0 -1
  10. package/src/cloud/database.ts +34 -0
  11. package/src/cloud/deployment.ts +8 -7
  12. package/src/cloud/dns.ts +0 -1
  13. package/src/cloud/docs.ts +3 -4
  14. package/src/cloud/email.ts +57 -40
  15. package/src/cloud/index.ts +9 -9
  16. package/src/cloud/jump-box.ts +0 -1
  17. package/src/cloud/lambda/ask/{index.js → index.cjs} +16 -15
  18. package/src/cloud/lambda/summarize/index.cjs +36 -0
  19. package/src/cloud/permissions.ts +3 -3
  20. package/src/cloud/queue.ts +73 -53
  21. package/src/cloud/redirects.ts +11 -4
  22. package/src/cloud/router-layer/nodejs/package.json +2 -2
  23. package/src/cloud/security.ts +19 -11
  24. package/src/edge/origin-request.ts +2 -1
  25. package/src/helpers.ts +147 -132
  26. package/dist/cloud/ai.d.ts +0 -10
  27. package/dist/cloud/cdn.d.ts +0 -44
  28. package/dist/cloud/cli.d.ts +0 -9
  29. package/dist/cloud/compute.d.ts +0 -17
  30. package/dist/cloud/deployment.d.ts +0 -14
  31. package/dist/cloud/dns.d.ts +0 -7
  32. package/dist/cloud/docs.d.ts +0 -9
  33. package/dist/cloud/email.d.ts +0 -10
  34. package/dist/cloud/file-system.d.ts +0 -12
  35. package/dist/cloud/index.d.ts +0 -41
  36. package/dist/cloud/jump-box.d.ts +0 -12
  37. package/dist/cloud/network.d.ts +0 -9
  38. package/dist/cloud/permissions.d.ts +0 -7
  39. package/dist/cloud/queue.d.ts +0 -24
  40. package/dist/cloud/redirects.d.ts +0 -9
  41. package/dist/cloud/security.d.ts +0 -14
  42. package/dist/cloud/storage.d.ts +0 -18
  43. package/dist/edge/origin-request.d.ts +0 -1
  44. package/dist/helpers.d.ts +0 -67
  45. package/dist/index.d.ts +0 -2
  46. package/dist/types.d.ts +0 -26
  47. package/src/cloud/lambda/summarize/index.js +0 -35
  48. /package/src/cloud/lambda/cli-setup/{index.js → index.cjs} +0 -0
package/dist/index.js CHANGED
@@ -5,13 +5,13 @@ import {CloudWatchLogsClient, DeleteLogGroupCommand, DescribeLogGroupsCommand} f
5
5
  import {EC2, _InstanceType as InstanceType} from "@aws-sdk/client-ec2";
6
6
  import {DescribeFileSystemsCommand, EFSClient} from "@aws-sdk/client-efs";
7
7
  import {IAM} from "@aws-sdk/client-iam";
8
- import {SSM} from "@aws-sdk/client-ssm";
9
8
  import {Lambda} from "@aws-sdk/client-lambda";
10
9
  import {ContactType, Route53Domains} from "@aws-sdk/client-route-53-domains";
11
10
  import {ListBucketsCommand, S3} from "@aws-sdk/client-s3";
11
+ import {SSM} from "@aws-sdk/client-ssm";
12
+ import {runCommand} from "@stacksjs/cli";
12
13
  import {config as config2} from "@stacksjs/config";
13
14
  import {err, handleError, ok} from "@stacksjs/error-handling";
14
- import {runCommand} from "@stacksjs/cli";
15
15
  import {log} from "@stacksjs/logging";
16
16
  import {path as p} from "@stacksjs/path";
17
17
  import {slug} from "@stacksjs/strings";
@@ -97,7 +97,7 @@ async function getJumpBoxInstanceId(name) {
97
97
  }
98
98
  ]
99
99
  });
100
- if (data.Reservations && data.Reservations[0] && data.Reservations[0].Instances && data.Reservations[0].Instances[0])
100
+ if (data.Reservations?.[0].Instances?.[0])
101
101
  return data.Reservations[0].Instances[0].InstanceId;
102
102
  return;
103
103
  }
@@ -133,14 +133,20 @@ async function deleteIamUsers() {
133
133
  const userName = user.UserName || "";
134
134
  log.info(`Deleting IAM user: ${userName}`);
135
135
  const policies = await iam.listAttachedUserPolicies({ UserName: userName });
136
- await Promise.all(policies.AttachedPolicies?.map((policy) => iam.detachUserPolicy({ UserName: userName, PolicyArn: policy.PolicyArn || "" })) || []);
136
+ await Promise.all(policies.AttachedPolicies?.map((policy) => iam.detachUserPolicy({
137
+ UserName: userName,
138
+ PolicyArn: policy.PolicyArn || ""
139
+ })) || []);
137
140
  const accessKeys = await iam.listAccessKeys({ UserName: userName });
138
- await Promise.all(accessKeys.AccessKeyMetadata?.map((key) => iam.deleteAccessKey({ UserName: userName, AccessKeyId: key.AccessKeyId || "" })) || []);
141
+ await Promise.all(accessKeys.AccessKeyMetadata?.map((key) => iam.deleteAccessKey({
142
+ UserName: userName,
143
+ AccessKeyId: key.AccessKeyId || ""
144
+ })) || []);
139
145
  return iam.deleteUser({ UserName: userName });
140
146
  });
141
147
  await Promise.all(promises).catch((error) => {
142
- console.error(`Error deleting user: ${error}`);
143
- return err(handleError("Error deleting Stacks IAM users", error));
148
+ console.error(error);
149
+ return err(handleError("Error deleting Stacks IAM users"));
144
150
  });
145
151
  return ok(`Stacks IAM users deleted for team ${teamName}`);
146
152
  }
@@ -165,18 +171,30 @@ async function deleteStacksBuckets() {
165
171
  try {
166
172
  const versions = await s3.listObjectVersions({ Bucket: bucketName });
167
173
  if (versions.Versions) {
168
- await Promise.all(versions.Versions.map((version) => s3.deleteObject({ Bucket: bucketName, Key: version.Key || "", VersionId: version.VersionId }))).catch((error) => handleError(error));
174
+ await Promise.all(versions.Versions.map((version) => s3.deleteObject({
175
+ Bucket: bucketName,
176
+ Key: version.Key || "",
177
+ VersionId: version.VersionId
178
+ }))).catch((error) => handleError(error));
169
179
  log.info(`Finished deleting versions from bucket ${bucketName}`);
170
180
  }
171
181
  log.info(`Deleting bucket ${bucketName} delete markers...`);
172
182
  if (versions.DeleteMarkers) {
173
- await Promise.all(versions.DeleteMarkers.map((marker) => s3.deleteObject({ Bucket: bucketName, Key: marker.Key || "", VersionId: marker.VersionId }))).catch((error) => handleError(error));
183
+ await Promise.all(versions.DeleteMarkers.map((marker) => s3.deleteObject({
184
+ Bucket: bucketName,
185
+ Key: marker.Key || "",
186
+ VersionId: marker.VersionId
187
+ }))).catch((error) => handleError(error));
174
188
  log.info(`Finished deleting delete markers from bucket ${bucketName}`);
175
189
  }
176
190
  const uploads = await s3.listMultipartUploads({ Bucket: bucketName });
177
191
  if (uploads.Uploads) {
178
192
  log.info("Aborting bucket multipart uploads...");
179
- await Promise.all(uploads.Uploads.map((upload) => s3.abortMultipartUpload({ Bucket: bucketName, Key: upload.Key || "", UploadId: upload.UploadId }))).catch((error) => handleError(error));
193
+ await Promise.all(uploads.Uploads.map((upload) => s3.abortMultipartUpload({
194
+ Bucket: bucketName,
195
+ Key: upload.Key || "",
196
+ UploadId: upload.UploadId
197
+ }))).catch((error) => handleError(error));
180
198
  log.info(`Finished aborting multipart uploads from bucket ${bucketName}`);
181
199
  }
182
200
  await s3.deleteBucket({ Bucket: bucketName }).catch((error) => handleError(error));
@@ -186,11 +204,13 @@ async function deleteStacksBuckets() {
186
204
  }
187
205
  });
188
206
  await Promise.all(promises).catch((error) => {
189
- return err(handleError("Error deleting stacks buckets", error));
207
+ console.error(error);
208
+ return err(handleError("Error deleting stacks buckets"));
190
209
  });
191
210
  return ok("Stacks buckets deleted");
192
211
  } catch (error) {
193
- return err(handleError("Error deleting stacks buckets", error));
212
+ console.error(error);
213
+ return err(handleError("Error deleting stacks buckets"));
194
214
  }
195
215
  }
196
216
  async function deleteStacksFunctions() {
@@ -205,7 +225,8 @@ async function deleteStacksFunctions() {
205
225
  log.info("Function is replicated, skipping...");
206
226
  return ok("CloudFront is still deleting the some functions. Try again later.");
207
227
  }
208
- return err(handleError("Error deleting stacks functions", error));
228
+ console.error(error);
229
+ return err(handleError("Error deleting stacks functions"));
209
230
  });
210
231
  return ok("Stacks functions deleted");
211
232
  }
@@ -221,7 +242,8 @@ async function deleteLogGroups() {
221
242
  }
222
243
  return ok("Log groups deleted");
223
244
  } catch (error) {
224
- return err(handleError("Error deleting log groups", error));
245
+ console.error(error);
246
+ return err(handleError("Error deleting log groups"));
225
247
  }
226
248
  }
227
249
  async function deleteParameterStore() {
@@ -234,7 +256,8 @@ async function deleteParameterStore() {
234
256
  return ok("No stacks parameters found");
235
257
  const promises = stacksParameters.map((param) => ssm.deleteParameter({ Name: param.Name || "" }));
236
258
  await Promise.all(promises).catch((error) => {
237
- return err(handleError("Error deleting parameter store", error));
259
+ console.error(error);
260
+ return err(handleError("Error deleting parameter store"));
238
261
  });
239
262
  return ok("Parameter store deleted");
240
263
  }
@@ -242,7 +265,8 @@ async function deleteCdkRemnants() {
242
265
  try {
243
266
  return ok(await runCommand(`bunx rimraf ${p.cloudPath("cdk.out/")} ${p.cloudPath("cdk.context.json")}`));
244
267
  } catch (error) {
245
- return err(handleError("Error deleting CDK remnants", error));
268
+ console.error(error);
269
+ return err(handleError("Error deleting CDK remnants"));
246
270
  }
247
271
  }
248
272
  async function hasBeenDeployed() {
@@ -251,7 +275,8 @@ async function hasBeenDeployed() {
251
275
  const response = await s3.send(new ListBucketsCommand({}));
252
276
  return ok(response.Buckets?.some((bucket) => bucket.Name?.includes(config2.app.name?.toLocaleLowerCase() || "stacks")) || false);
253
277
  } catch (error) {
254
- return err(handleError("Error checking if the app has been deployed", error));
278
+ console.error(error);
279
+ return err(handleError("Error checking if the app has been deployed"));
255
280
  }
256
281
  }
257
282
  async function getJumpBoxInstanceProfileName() {
@@ -274,11 +299,9 @@ async function addJumpBox(stackName) {
274
299
  if (!r.value)
275
300
  return err("Security group not found when adding jump-box");
276
301
  const result = await getSecurityGroupId(r.value);
277
- let sgId;
278
302
  if (result.isErr())
279
303
  return err(result.error);
280
- else
281
- sgId = result.value;
304
+ const sgId = result.value;
282
305
  if (!sgId)
283
306
  return err("Security group not found when adding jump-box");
284
307
  const client = new EFSClient({ region: "us-east-1" });
@@ -329,7 +352,7 @@ git clone https://github.com/stacksjs/stacks.git /mnt/efs
329
352
  Name: jumpBoxInstanceProfileName
330
353
  }
331
354
  });
332
- return instance.Instances && instance.Instances[0] ? ok(`Jump-box created with id ${instance.Instances[0].InstanceId}`) : err("Jump-box creation failed");
355
+ return instance.Instances?.[0] ? ok(`Jump-box created with id ${instance.Instances[0].InstanceId}`) : err("Jump-box creation failed");
333
356
  }
334
357
  async function getJumpBoxSecurityGroupName() {
335
358
  const jumpBoxId = await getJumpBoxInstanceId();
@@ -337,10 +360,10 @@ async function getJumpBoxSecurityGroupName() {
337
360
  return err("Jump-box not found");
338
361
  const ec2 = new EC2({ region: "us-east-1" });
339
362
  const data = await ec2.describeInstances({ InstanceIds: [jumpBoxId] });
340
- if (data.Reservations && data.Reservations[0] && data.Reservations[0].Instances && data.Reservations[0].Instances[0]) {
363
+ if (data.Reservations?.[0].Instances?.[0]) {
341
364
  const instance = data.Reservations[0].Instances[0];
342
365
  const securityGroups = instance.SecurityGroups;
343
- if (securityGroups && securityGroups[0])
366
+ if (securityGroups?.[0])
344
367
  return ok(securityGroups[0].GroupName);
345
368
  }
346
369
  return err("Security group not found");
@@ -348,10 +371,10 @@ async function getJumpBoxSecurityGroupName() {
348
371
  async function getSecurityGroupFromInstanceId(instanceId) {
349
372
  const ec2 = new EC2({ region: "us-east-1" });
350
373
  const data = await ec2.describeInstances({ InstanceIds: [instanceId] });
351
- if (data.Reservations && data.Reservations[0] && data.Reservations[0].Instances && data.Reservations[0].Instances[0]) {
374
+ if (data.Reservations?.[0].Instances?.[0]) {
352
375
  const instance = data.Reservations[0].Instances[0];
353
376
  const securityGroups = instance.SecurityGroups;
354
- if (securityGroups && securityGroups[0])
377
+ if (securityGroups?.[0])
355
378
  return securityGroups[0].GroupId;
356
379
  }
357
380
  return;
@@ -359,13 +382,17 @@ async function getSecurityGroupFromInstanceId(instanceId) {
359
382
  async function isFirstDeployment() {
360
383
  const stackName = cloudName;
361
384
  const cloudFormation = new CloudFormation;
362
- const data = await cloudFormation.listStacks({ StackStatusFilter: ["CREATE_COMPLETE", "UPDATE_COMPLETE"] });
385
+ const data = await cloudFormation.listStacks({
386
+ StackStatusFilter: ["CREATE_COMPLETE", "UPDATE_COMPLETE"]
387
+ });
363
388
  const isStacksCloudPresent = data.StackSummaries?.some((stack) => stack.StackName === stackName);
364
389
  return !isStacksCloudPresent;
365
390
  }
366
391
  async function isFailedState() {
367
392
  const cloudFormation = new CloudFormation;
368
- const data = await cloudFormation.listStacks({ StackStatusFilter: ["CREATE_FAILED", "UPDATE_FAILED", "ROLLBACK_COMPLETE", "UPDATE_ROLLBACK_COMPLETE"] });
393
+ const data = await cloudFormation.listStacks({
394
+ StackStatusFilter: ["CREATE_FAILED", "UPDATE_FAILED", "ROLLBACK_COMPLETE", "UPDATE_ROLLBACK_COMPLETE"]
395
+ });
369
396
  const isStacksCloudPresent = data.StackSummaries?.some((stack) => stack.StackName === cloudName);
370
397
  return !isStacksCloudPresent;
371
398
  }
@@ -392,12 +419,12 @@ async function getOrCreateTimestamp() {
392
419
  var appEnv = config2.app.env === "local" ? "dev" : config2.app.env;
393
420
  var cloudName = `stacks-cloud-${appEnv}`;
394
421
  // src/cloud/index.ts
395
- import {Stack as Stack2} from "aws-cdk-lib";
396
422
  import {config as config20} from "@stacksjs/config";
423
+ import {Stack as Stack2} from "aws-cdk-lib";
397
424
 
398
425
  // src/cloud/ai.ts
399
- import {Duration, CfnOutput as Output, aws_iam as iam, aws_lambda as lambda} from "aws-cdk-lib";
400
426
  import {config as config4} from "@stacksjs/config";
427
+ import {Duration, CfnOutput as Output, aws_iam as iam, aws_lambda as lambda} from "aws-cdk-lib";
401
428
 
402
429
  class AiStack {
403
430
  askAiUrl;
@@ -410,17 +437,12 @@ class AiStack {
410
437
  });
411
438
  const bedrockAccessPolicy = new iam.PolicyStatement({
412
439
  effect: iam.Effect.ALLOW,
413
- actions: [
414
- "bedrock:InvokeModel",
415
- "bedrock:InvokeModelWithResponseStream"
416
- ],
440
+ actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
417
441
  resources: config4.ai.models?.map((model) => `arn:aws:bedrock:us-east-1::foundation-model/${model}`)
418
442
  });
419
443
  const bedrockAccessRole = new iam.Role(scope, "BedrockAccessRole", {
420
444
  assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
421
- managedPolicies: [
422
- iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
423
- ]
445
+ managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")]
424
446
  });
425
447
  bedrockAccessRole.addToPolicy(bedrockAccessPolicy);
426
448
  const askAi = new lambda.Function(scope, "AskAiFunction", {
@@ -473,11 +495,19 @@ class AiStack {
473
495
  }
474
496
 
475
497
  // src/cloud/cdn.ts
476
- import {Duration as Duration2, Fn, CfnOutput as Output2, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_route53 as route53, aws_route53_targets as targets} from "aws-cdk-lib";
477
498
  import {config as config6} from "@stacksjs/config";
478
- import {hasFiles} from "@stacksjs/storage";
479
- import {path as p2} from "@stacksjs/path";
480
499
  import {env as env2} from "@stacksjs/env";
500
+ import {path as p2} from "@stacksjs/path";
501
+ import {hasFiles} from "@stacksjs/storage";
502
+ import {
503
+ Duration as Duration2,
504
+ Fn,
505
+ CfnOutput as Output2,
506
+ aws_cloudfront as cloudfront,
507
+ aws_cloudfront_origins as origins,
508
+ aws_route53 as route53,
509
+ aws_route53_targets as targets
510
+ } from "aws-cdk-lib";
481
511
  import * as kinesis from "aws-cdk-lib/aws-kinesis";
482
512
 
483
513
  class CdnStack {
@@ -506,9 +536,7 @@ class CdnStack {
506
536
  encryption: kinesis.StreamEncryption.UNENCRYPTED
507
537
  });
508
538
  this.realtimeLogConfig = new cloudfront.RealtimeLogConfig(scope, "StacksRealTimeLogConfig", {
509
- endPoints: [
510
- cloudfront.Endpoint.fromKinesisStream(logStream)
511
- ],
539
+ endPoints: [cloudfront.Endpoint.fromKinesisStream(logStream)],
512
540
  fields: [
513
541
  "timestamp",
514
542
  "c-ip",
@@ -640,7 +668,7 @@ class CdnStack {
640
668
  }
641
669
  }
642
670
  shouldDeployApi() {
643
- return config6.cloud.api?.deploy;
671
+ return config6.api?.deploy;
644
672
  }
645
673
  apiBehaviorOptions(scope, props) {
646
674
  const hostname = `api.${props.domain}`;
@@ -770,7 +798,26 @@ class CdnStack {
770
798
  additionalBehaviors(scope, props) {
771
799
  let behaviorOptions = {};
772
800
  if (this.shouldDeployApi()) {
773
- const keysToRemove = ["_HANDLER", "_X_AMZN_TRACE_ID", "AWS_REGION", "AWS_EXECUTION_ENV", "AWS_LAMBDA_FUNCTION_NAME", "AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "AWS_LAMBDA_FUNCTION_VERSION", "AWS_LAMBDA_INITIALIZATION_TYPE", "AWS_LAMBDA_LOG_GROUP_NAME", "AWS_LAMBDA_LOG_STREAM_NAME", "AWS_ACCESS_KEY", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_LAMBDA_RUNTIME_API", "LAMBDA_TASK_ROOT", "LAMBDA_RUNTIME_DIR", "_"];
801
+ const keysToRemove = [
802
+ "_HANDLER",
803
+ "_X_AMZN_TRACE_ID",
804
+ "AWS_REGION",
805
+ "AWS_EXECUTION_ENV",
806
+ "AWS_LAMBDA_FUNCTION_NAME",
807
+ "AWS_LAMBDA_FUNCTION_MEMORY_SIZE",
808
+ "AWS_LAMBDA_FUNCTION_VERSION",
809
+ "AWS_LAMBDA_INITIALIZATION_TYPE",
810
+ "AWS_LAMBDA_LOG_GROUP_NAME",
811
+ "AWS_LAMBDA_LOG_STREAM_NAME",
812
+ "AWS_ACCESS_KEY",
813
+ "AWS_ACCESS_KEY_ID",
814
+ "AWS_SECRET_ACCESS_KEY",
815
+ "AWS_SESSION_TOKEN",
816
+ "AWS_LAMBDA_RUNTIME_API",
817
+ "LAMBDA_TASK_ROOT",
818
+ "LAMBDA_RUNTIME_DIR",
819
+ "_"
820
+ ];
774
821
  keysToRemove.forEach((key) => delete env2[key]);
775
822
  behaviorOptions = this.apiBehaviorOptions(scope, props);
776
823
  }
@@ -840,13 +887,240 @@ class CliStack {
840
887
  }
841
888
  }
842
889
 
890
+ // src/cloud/compute.ts
891
+ import {env as env4} from "@stacksjs/env";
892
+ import {path as p3} from "@stacksjs/path";
893
+ import {
894
+ Duration as Duration4,
895
+ CfnOutput as Output4,
896
+ RemovalPolicy,
897
+ aws_ec2 as ec2,
898
+ aws_ecs as ecs,
899
+ aws_route53 as route532,
900
+ aws_route53_targets as route53Targets,
901
+ aws_secretsmanager as secretsmanager
902
+ } from "aws-cdk-lib";
903
+ import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
904
+ import {LogGroup} from "aws-cdk-lib/aws-logs";
905
+
906
+ class ComputeStack {
907
+ lb;
908
+ cluster;
909
+ taskDefinition;
910
+ constructor(scope, props) {
911
+ const vpc = props.vpc;
912
+ const fileSystem = props.fileSystem;
913
+ if (!fileSystem)
914
+ throw new Error("The file system is missing. Please make sure it was created properly.");
915
+ this.cluster = new ecs.Cluster(scope, "StacksCluster", {
916
+ clusterName: `${props.slug}-${props.appEnv}-web-server-cluster`,
917
+ vpc
918
+ });
919
+ this.taskDefinition = new ecs.FargateTaskDefinition(scope, "TaskDefinition", {
920
+ family: `${props.appName}-${props.appEnv}-api`,
921
+ memoryLimitMiB: 512,
922
+ cpu: 256,
923
+ runtimePlatform: {
924
+ cpuArchitecture: ecs.CpuArchitecture.ARM64
925
+ }
926
+ });
927
+ const container = this.taskDefinition.addContainer("WebServerContainer", {
928
+ containerName: `${props.appName}-${props.appEnv}-api`,
929
+ image: ecs.ContainerImage.fromAsset(p3.frameworkPath("server")),
930
+ logging: new ecs.AwsLogDriver({
931
+ streamPrefix: `${props.appName}-${props.appEnv}-web-server-logs`,
932
+ logGroup: new LogGroup(scope, "StacksApiLogs", {
933
+ logGroupName: "/aws/ecs/stacks-api",
934
+ removalPolicy: RemovalPolicy.DESTROY
935
+ })
936
+ }),
937
+ healthCheck: {
938
+ command: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"],
939
+ interval: Duration4.seconds(10),
940
+ timeout: Duration4.seconds(5),
941
+ retries: 3,
942
+ startPeriod: Duration4.seconds(10)
943
+ }
944
+ });
945
+ container.addPortMappings({
946
+ containerPort: 3000,
947
+ hostPort: 3000
948
+ });
949
+ const serviceSecurityGroup = new ec2.SecurityGroup(scope, "ServiceSecurityGroup", {
950
+ securityGroupName: `${props.appName}-${props.appEnv}-api-service-sg`,
951
+ vpc,
952
+ description: "Stacks Security Group for API Service"
953
+ });
954
+ const publicLoadBalancerSG = new ec2.SecurityGroup(scope, "PublicLoadBalancerSG", {
955
+ securityGroupName: `${props.appName}-${props.appEnv}-public-load-balancer-sg`,
956
+ vpc,
957
+ description: "Access to the public facing load balancer"
958
+ });
959
+ serviceSecurityGroup.addIngressRule(publicLoadBalancerSG, ec2.Port.allTraffic(), "Ingress from the public ALB");
960
+ this.lb = new elbv2.ApplicationLoadBalancer(scope, "ApplicationLoadBalancer", {
961
+ http2Enabled: true,
962
+ loadBalancerName: `${props.appName}-${props.appEnv}-alb`,
963
+ vpc,
964
+ vpcSubnets: {
965
+ subnets: vpc.selectSubnets({
966
+ subnetType: ec2.SubnetType.PUBLIC,
967
+ onePerAz: true
968
+ }).subnets
969
+ },
970
+ internetFacing: true,
971
+ idleTimeout: Duration4.seconds(30),
972
+ securityGroup: publicLoadBalancerSG
973
+ });
974
+ new route532.ARecord(scope, "ApiDomainAliasRecord", {
975
+ zone: props.zone,
976
+ recordName: "api",
977
+ target: route532.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(this.lb))
978
+ });
979
+ const serviceTargetGroup = new elbv2.ApplicationTargetGroup(scope, "ServiceTargetGroup", {
980
+ targetGroupName: `${props.appName}-${props.appEnv}-api-tg`,
981
+ vpc,
982
+ targetType: elbv2.TargetType.IP,
983
+ protocol: elbv2.ApplicationProtocol.HTTP,
984
+ port: 3000,
985
+ healthCheck: {
986
+ interval: Duration4.seconds(6),
987
+ path: "/api/health",
988
+ protocol: elbv2.Protocol.HTTP,
989
+ timeout: Duration4.seconds(5),
990
+ healthyThresholdCount: 2,
991
+ unhealthyThresholdCount: 10
992
+ }
993
+ });
994
+ const service = new ecs.FargateService(scope, "StacksApiService", {
995
+ serviceName: `${props.appName}-${props.appEnv}-api-service`,
996
+ cluster: this.cluster,
997
+ taskDefinition: this.taskDefinition,
998
+ desiredCount: 1,
999
+ assignPublicIp: true,
1000
+ maxHealthyPercent: 200,
1001
+ vpcSubnets: vpc.selectSubnets({
1002
+ subnetType: ec2.SubnetType.PUBLIC,
1003
+ onePerAz: true
1004
+ }),
1005
+ minHealthyPercent: 75,
1006
+ securityGroups: [serviceSecurityGroup]
1007
+ });
1008
+ service.attachToApplicationTargetGroup(serviceTargetGroup);
1009
+ publicLoadBalancerSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.allTraffic());
1010
+ this.lb.addListener("HttpsListener", {
1011
+ port: 443,
1012
+ certificates: [props.certificate],
1013
+ defaultAction: elbv2.ListenerAction.forward([serviceTargetGroup])
1014
+ });
1015
+ this.lb.addListener("HttpListener", {
1016
+ port: 80,
1017
+ defaultAction: elbv2.ListenerAction.forward([serviceTargetGroup])
1018
+ });
1019
+ props.fileSystem.connections.allowFromAnyIpv4(ec2.Port.tcp(2049));
1020
+ const volumeName = `${props.slug}-${props.appEnv}-efs`;
1021
+ this.taskDefinition.addVolume({
1022
+ name: volumeName,
1023
+ efsVolumeConfiguration: {
1024
+ fileSystemId: props.fileSystem.fileSystemId
1025
+ }
1026
+ });
1027
+ container.addMountPoints({
1028
+ sourceVolume: volumeName,
1029
+ containerPath: "/mnt/efs",
1030
+ readOnly: false
1031
+ });
1032
+ const scaling = service.autoScaleTaskCount({ maxCapacity: 2 });
1033
+ scaling.scaleOnCpuUtilization("CpuScaling", {
1034
+ targetUtilizationPercent: 50,
1035
+ scaleInCooldown: Duration4.seconds(60),
1036
+ scaleOutCooldown: Duration4.seconds(60)
1037
+ });
1038
+ scaling.scaleOnMemoryUtilization("MemoryScaling", {
1039
+ targetUtilizationPercent: 60,
1040
+ scaleInCooldown: Duration4.seconds(60),
1041
+ scaleOutCooldown: Duration4.seconds(60)
1042
+ });
1043
+ const keysToRemove = [
1044
+ "_HANDLER",
1045
+ "_X_AMZN_TRACE_ID",
1046
+ "AWS_REGION",
1047
+ "AWS_EXECUTION_ENV",
1048
+ "AWS_LAMBDA_FUNCTION_NAME",
1049
+ "AWS_LAMBDA_FUNCTION_MEMORY_SIZE",
1050
+ "AWS_LAMBDA_FUNCTION_VERSION",
1051
+ "AWS_LAMBDA_INITIALIZATION_TYPE",
1052
+ "AWS_LAMBDA_LOG_GROUP_NAME",
1053
+ "AWS_LAMBDA_LOG_STREAM_NAME",
1054
+ "AWS_ACCESS_KEY",
1055
+ "AWS_ACCESS_KEY_ID",
1056
+ "AWS_SECRET_ACCESS_KEY",
1057
+ "AWS_SESSION_TOKEN",
1058
+ "AWS_LAMBDA_RUNTIME_API",
1059
+ "LAMBDA_TASK_ROOT",
1060
+ "LAMBDA_RUNTIME_DIR",
1061
+ "_"
1062
+ ];
1063
+ keysToRemove.forEach((key) => delete env4[key]);
1064
+ const secrets = new secretsmanager.Secret(scope, "StacksSecrets", {
1065
+ secretName: `${props.slug}-${props.appEnv}-secrets`,
1066
+ description: "Secrets for the Stacks application",
1067
+ generateSecretString: {
1068
+ secretStringTemplate: JSON.stringify(env4),
1069
+ generateStringKey: Object.keys(env4).join(",").length.toString()
1070
+ }
1071
+ });
1072
+ secrets.grantRead(service.taskDefinition.executionRole);
1073
+ container.addEnvironment("SECRETS_ARN", secrets.secretArn);
1074
+ const apiPrefix = "api";
1075
+ new Output4(scope, "ApiUrl", {
1076
+ value: `https://${props.domain}/${apiPrefix}`,
1077
+ description: "The URL of the deployed application"
1078
+ });
1079
+ new Output4(scope, "ApiVanityUrl", {
1080
+ value: `http://${this.lb.loadBalancerDnsName}`,
1081
+ description: "The Vanity URL / DNS name of the load balancer"
1082
+ });
1083
+ }
1084
+ }
1085
+
1086
+ // src/cloud/deployment.ts
1087
+ import {config as config8} from "@stacksjs/config";
1088
+ import {websiteSourceHash} from "@stacksjs/utils";
1089
+ import {AssetHashType, aws_s3_deployment as s3deploy} from "aws-cdk-lib";
1090
+
1091
+ class DeploymentStack {
1092
+ privateSource;
1093
+ docsSource;
1094
+ websiteSource;
1095
+ constructor(scope, props) {
1096
+ this.privateSource = "../../private";
1097
+ this.docsSource = "../docs/dist/";
1098
+ this.websiteSource = config8.app.docMode === true ? this.docsSource : "../views/dist/";
1099
+ new s3deploy.BucketDeployment(scope, "Website", {
1100
+ sources: [
1101
+ s3deploy.Source.asset(this.websiteSource, {
1102
+ assetHash: websiteSourceHash(),
1103
+ assetHashType: AssetHashType.CUSTOM
1104
+ })
1105
+ ],
1106
+ destinationBucket: props.publicBucket,
1107
+ distribution: props.cdn,
1108
+ distributionPaths: ["/*"]
1109
+ });
1110
+ new s3deploy.BucketDeployment(scope, "PrivateFiles", {
1111
+ sources: [s3deploy.Source.asset(this.privateSource)],
1112
+ destinationBucket: props.privateBucket
1113
+ });
1114
+ }
1115
+ }
1116
+
843
1117
  // src/cloud/dns.ts
844
- import {RemovalPolicy, aws_route53 as route532, aws_s3 as s3, aws_route53_targets as targets2} from "aws-cdk-lib";
1118
+ import {RemovalPolicy as RemovalPolicy2, aws_route53 as route533, aws_s3 as s3, aws_route53_targets as targets2} from "aws-cdk-lib";
845
1119
 
846
1120
  class DnsStack {
847
1121
  zone;
848
1122
  constructor(scope, props) {
849
- this.zone = route532.PublicHostedZone.fromLookup(scope, "AppUrlHostedZone", {
1123
+ this.zone = route533.PublicHostedZone.fromLookup(scope, "AppUrlHostedZone", {
850
1124
  domainName: props.domain
851
1125
  });
852
1126
  const wwwBucket = new s3.Bucket(scope, "WwwBucket", {
@@ -855,47 +1129,47 @@ class DnsStack {
855
1129
  hostName: props.domain,
856
1130
  protocol: s3.RedirectProtocol.HTTPS
857
1131
  },
858
- removalPolicy: RemovalPolicy.DESTROY,
1132
+ removalPolicy: RemovalPolicy2.DESTROY,
859
1133
  autoDeleteObjects: true
860
1134
  });
861
- new route532.ARecord(scope, "WwwAliasRecord", {
1135
+ new route533.ARecord(scope, "WwwAliasRecord", {
862
1136
  recordName: `www.${props.domain}`,
863
1137
  zone: this.zone,
864
- target: route532.RecordTarget.fromAlias(new targets2.BucketWebsiteTarget(wwwBucket))
1138
+ target: route533.RecordTarget.fromAlias(new targets2.BucketWebsiteTarget(wwwBucket))
865
1139
  });
866
- new route532.ARecord(scope, "StoreAliasRecord", {
1140
+ new route533.ARecord(scope, "StoreAliasRecord", {
867
1141
  recordName: `store.${props.domain}`,
868
1142
  zone: this.zone,
869
- target: route532.RecordTarget.fromIpAddresses("137.66.37.136")
1143
+ target: route533.RecordTarget.fromIpAddresses("137.66.37.136")
870
1144
  });
871
1145
  }
872
1146
  }
873
1147
 
874
1148
  // src/cloud/docs.ts
875
- import {AssetHashType, CfnOutput as Output4, RemovalPolicy as RemovalPolicy2, aws_lambda as lambda3} from "aws-cdk-lib";
876
- import {config as config8} from "@stacksjs/config";
877
- import {path as p3} from "@stacksjs/path";
1149
+ import {config as config10} from "@stacksjs/config";
1150
+ import {path as p4} from "@stacksjs/path";
878
1151
  import {storage as storage3} from "@stacksjs/storage";
879
1152
  import {originRequestFunctionHash} from "@stacksjs/utils";
1153
+ import {AssetHashType as AssetHashType2, CfnOutput as Output5, RemovalPolicy as RemovalPolicy3, aws_lambda as lambda3} from "aws-cdk-lib";
880
1154
 
881
1155
  class DocsStack {
882
1156
  originRequestFunction;
883
1157
  constructor(scope, props) {
884
- const docsPrefix = config8.app.docMode ? "" : config8.docs.base;
1158
+ const docsPrefix = config10.app.docMode ? "" : config10.docs.base;
885
1159
  this.originRequestFunction = new lambda3.Function(scope, "OriginRequestFunction", {
886
1160
  functionName: `${props.slug}-${props.appEnv}-origin-request-${props.timestamp}`,
887
1161
  description: "The Stacks Origin Request function that prettifies URLs",
888
1162
  runtime: lambda3.Runtime.NODEJS_18_X,
889
1163
  handler: "dist/origin-request.handler",
890
- code: lambda3.Code.fromAsset(p3.frameworkCloudPath("dist.zip"), {
1164
+ code: lambda3.Code.fromAsset(p4.frameworkCloudPath("dist.zip"), {
891
1165
  assetHash: originRequestFunctionHash(),
892
- assetHashType: AssetHashType.CUSTOM
1166
+ assetHashType: AssetHashType2.CUSTOM
893
1167
  })
894
1168
  });
895
1169
  const cfnOriginRequestFunction = this.originRequestFunction.node.defaultChild;
896
- cfnOriginRequestFunction.applyRemovalPolicy(RemovalPolicy2.RETAIN);
897
- if (!config8.app.docMode && storage3.hasFiles(p3.projectPath("docs"))) {
898
- new Output4(scope, "DocsUrl", {
1170
+ cfnOriginRequestFunction.applyRemovalPolicy(RemovalPolicy3.RETAIN);
1171
+ if (!config10.app.docMode && storage3.hasFiles(p4.projectPath("docs"))) {
1172
+ new Output5(scope, "DocsUrl", {
899
1173
  value: `https://${props.domain}/${docsPrefix}`,
900
1174
  description: "The URL of the deployed documentation"
901
1175
  });
@@ -903,479 +1177,76 @@ class DocsStack {
903
1177
  }
904
1178
  }
905
1179
 
906
- // src/cloud/storage.ts
907
- import {RemovalPolicy as RemovalPolicy3, Tags, aws_backup as backup, aws_iam as iam2, aws_s3 as s32} from "aws-cdk-lib";
1180
+ // src/cloud/email.ts
1181
+ import {config as config12} from "@stacksjs/config";
1182
+ import {
1183
+ Duration as Duration5,
1184
+ RemovalPolicy as RemovalPolicy4,
1185
+ Stack,
1186
+ Tags,
1187
+ aws_iam as iam2,
1188
+ aws_lambda as lambda4,
1189
+ aws_route53 as route534,
1190
+ aws_s3 as s32,
1191
+ aws_s3_notifications as s3n,
1192
+ aws_ses as ses
1193
+ } from "aws-cdk-lib";
908
1194
 
909
- class StorageStack {
910
- publicBucket;
911
- privateBucket;
912
- logBucket;
913
- bucketPrefix;
914
- vault;
915
- backupPlan;
916
- backupRole;
1195
+ class EmailStack {
1196
+ emailBucket;
917
1197
  constructor(scope, props) {
918
- this.bucketPrefix = `${props.slug}-${props.appEnv}`;
919
- this.publicBucket = new s32.Bucket(scope, "PublicBucket", {
920
- bucketName: `${this.bucketPrefix}-public-${props.timestamp}`,
921
- versioned: true,
922
- autoDeleteObjects: true,
923
- removalPolicy: RemovalPolicy3.DESTROY,
924
- encryption: s32.BucketEncryption.S3_MANAGED
925
- });
926
- Tags.of(this.publicBucket).add("daily-backup", "true");
927
- this.privateBucket = new s32.Bucket(scope, "PrivateBucket", {
928
- bucketName: `${this.bucketPrefix}-private-${props.timestamp}`,
1198
+ const bucketPrefix = `${props.slug}-${props.appEnv}`;
1199
+ this.emailBucket = new s32.Bucket(scope, "EmailBucket", {
1200
+ bucketName: `${bucketPrefix}-email-${props.timestamp}`,
929
1201
  versioned: true,
930
- removalPolicy: RemovalPolicy3.DESTROY,
1202
+ removalPolicy: RemovalPolicy4.DESTROY,
931
1203
  autoDeleteObjects: true,
932
1204
  encryption: s32.BucketEncryption.S3_MANAGED,
933
- enforceSSL: true,
934
- publicReadAccess: false,
935
- blockPublicAccess: {
936
- blockPublicAcls: true,
937
- blockPublicPolicy: true,
938
- ignorePublicAcls: true,
939
- restrictPublicBuckets: true
940
- }
941
- });
942
- Tags.of(this.privateBucket).add("daily-backup", "true");
943
- this.logBucket = new s32.Bucket(scope, "LogsBucket", {
944
- bucketName: `${this.bucketPrefix}-logs-${props.timestamp}`,
945
- removalPolicy: RemovalPolicy3.RETAIN,
946
- blockPublicAccess: new s32.BlockPublicAccess({
947
- blockPublicAcls: false,
948
- ignorePublicAcls: true,
949
- blockPublicPolicy: true,
950
- restrictPublicBuckets: true
951
- }),
952
- objectOwnership: s32.ObjectOwnership.BUCKET_OWNER_PREFERRED
1205
+ lifecycleRules: [
1206
+ {
1207
+ id: "24h",
1208
+ enabled: true,
1209
+ expiration: Duration5.days(1),
1210
+ noncurrentVersionExpiration: Duration5.days(1),
1211
+ prefix: "today/"
1212
+ },
1213
+ {
1214
+ id: "Intelligent transition for Inbox",
1215
+ enabled: true,
1216
+ prefix: "inbox/",
1217
+ transitions: [
1218
+ {
1219
+ storageClass: s32.StorageClass.INTELLIGENT_TIERING,
1220
+ transitionAfter: Duration5.days(0)
1221
+ }
1222
+ ]
1223
+ },
1224
+ {
1225
+ id: "Intelligent transition for Sent",
1226
+ enabled: true,
1227
+ prefix: "sent/",
1228
+ transitions: [
1229
+ {
1230
+ storageClass: s32.StorageClass.INTELLIGENT_TIERING,
1231
+ transitionAfter: Duration5.days(0)
1232
+ }
1233
+ ]
1234
+ }
1235
+ ]
953
1236
  });
954
- Tags.of(this.logBucket).add("daily-backup", "true");
955
- this.backupRole = this.createBackupRole(scope);
956
- this.vault = new backup.BackupVault(scope, "BackupVault", {
957
- backupVaultName: `${props.slug}-${props.appEnv}-daily-backup-vault`,
958
- encryptionKey: props.kmsKey,
959
- removalPolicy: RemovalPolicy3.DESTROY
960
- });
961
- this.backupPlan = backup.BackupPlan.daily35DayRetention(scope, "BackupPlan", this.vault);
962
- this.backupPlan.addSelection("Selection", {
963
- role: this.backupRole,
964
- resources: [backup.BackupResource.fromTag("daily-backup", "true")]
965
- });
966
- }
967
- createBackupRole(scope) {
968
- const backupRole = new iam2.Role(scope, "BackupRole", {
969
- assumedBy: new iam2.ServicePrincipal("backup.amazonaws.com")
970
- });
971
- backupRole.addToPolicy(new iam2.PolicyStatement({
972
- actions: [
973
- "s3:GetInventoryConfiguration",
974
- "s3:PutInventoryConfiguration",
975
- "s3:ListBucketVersions",
976
- "s3:ListBucket",
977
- "s3:GetBucketVersioning",
978
- "s3:GetBucketNotification",
979
- "s3:PutBucketNotification",
980
- "s3:GetBucketLocation",
981
- "s3:GetBucketTagging"
982
- ],
983
- resources: ["arn:aws:s3:::*"],
984
- sid: "S3BucketBackupPermissions"
985
- }));
986
- backupRole.addToPolicy(new iam2.PolicyStatement({
987
- actions: [
988
- "s3:GetObjectAcl",
989
- "s3:GetObject",
990
- "s3:GetObjectVersionTagging",
991
- "s3:GetObjectVersionAcl",
992
- "s3:GetObjectTagging",
993
- "s3:GetObjectVersion"
994
- ],
995
- resources: ["arn:aws:s3:::*/*"],
996
- sid: "S3ObjectBackupPermissions"
997
- }));
998
- backupRole.addToPolicy(new iam2.PolicyStatement({
999
- actions: ["s3:ListAllMyBuckets"],
1000
- resources: ["*"],
1001
- sid: "S3GlobalPermissions"
1002
- }));
1003
- backupRole.addToPolicy(new iam2.PolicyStatement({
1004
- actions: ["kms:Decrypt", "kms:DescribeKey"],
1005
- resources: ["*"],
1006
- sid: "KMSBackupPermissions",
1007
- conditions: {
1008
- StringLike: {
1009
- "kms:ViaService": "s3.*.amazonaws.com"
1010
- }
1011
- }
1012
- }));
1013
- backupRole.addToPolicy(new iam2.PolicyStatement({
1014
- actions: [
1015
- "events:DescribeRule",
1016
- "events:EnableRule",
1017
- "events:PutRule",
1018
- "events:DeleteRule",
1019
- "events:PutTargets",
1020
- "events:RemoveTargets",
1021
- "events:ListTargetsByRule",
1022
- "events:DisableRule"
1023
- ],
1024
- resources: ["arn:aws:events:*:*:rule/AwsBackupManagedRule*"],
1025
- sid: "EventsPermissions"
1026
- }));
1027
- backupRole.addToPolicy(new iam2.PolicyStatement({
1028
- actions: ["cloudwatch:GetMetricData", "events:ListRules"],
1029
- resources: ["*"],
1030
- sid: "EventsMetricsGlobalPermissions"
1031
- }));
1032
- return backupRole;
1033
- }
1034
- }
1035
-
1036
- // src/cloud/security.ts
1037
- import {config as config10} from "@stacksjs/config";
1038
- import {Duration as Duration4, RemovalPolicy as RemovalPolicy4, Tags as Tags2, aws_certificatemanager as acm, aws_kms as kms, aws_wafv2 as wafv2} from "aws-cdk-lib";
1039
-
1040
- class SecurityStack {
1041
- firewall;
1042
- kmsKey;
1043
- certificate;
1044
- constructor(scope, props) {
1045
- const firewallOptions = config10.cloud.firewall;
1046
- if (!firewallOptions)
1047
- throw new Error("No firewall options found in config");
1048
- const options = {
1049
- defaultAction: { allow: {} },
1050
- scope: "CLOUDFRONT",
1051
- visibilityConfig: {
1052
- sampledRequestsEnabled: true,
1053
- cloudWatchMetricsEnabled: true,
1054
- metricName: "firewallMetric"
1055
- },
1056
- rules: this.getFirewallRules()
1057
- };
1058
- this.firewall = new wafv2.CfnWebACL(scope, "StacksWebFirewall", options);
1059
- Tags2.of(this.firewall).add("Name", "waf-cloudfront", { priority: 300 });
1060
- Tags2.of(this.firewall).add("Purpose", "CloudFront", { priority: 300 });
1061
- Tags2.of(this.firewall).add("CreatedBy", "CloudFormation", { priority: 300 });
1062
- this.kmsKey = new kms.Key(scope, "EncryptionKey", {
1063
- alias: "stacks-encryption-key",
1064
- description: "KMS key for Stacks Cloud",
1065
- enableKeyRotation: true,
1066
- removalPolicy: RemovalPolicy4.DESTROY,
1067
- pendingWindow: Duration4.days(30)
1068
- });
1069
- this.certificate = new acm.Certificate(scope, "Certificate", {
1070
- domainName: props.domain,
1071
- validation: acm.CertificateValidation.fromDns(props.zone),
1072
- subjectAlternativeNames: [`www.${props.domain}`, `api.${props.domain}`]
1073
- });
1074
- }
1075
- getFirewallRules() {
1076
- const rules = [];
1077
- const priorities = [];
1078
- if (config10.security.firewall?.countryCodes?.length) {
1079
- priorities.push(1);
1080
- rules.push({
1081
- name: "CountryRule",
1082
- priority: priorities.length,
1083
- statement: {
1084
- geoMatchStatement: {
1085
- countryCodes: config10.security.firewall.countryCodes
1086
- }
1087
- },
1088
- action: {
1089
- block: {}
1090
- },
1091
- visibilityConfig: {
1092
- sampledRequestsEnabled: true,
1093
- cloudWatchMetricsEnabled: true,
1094
- metricName: "CountryRule"
1095
- }
1096
- });
1097
- }
1098
- if (config10.security.firewall?.ipAddresses?.length) {
1099
- const ipSet = new wafv2.CfnIPSet(this, "IpSet", {
1100
- name: "IpSet",
1101
- description: "IP Set",
1102
- scope: "CLOUDFRONT",
1103
- addresses: config10.security.firewall.ipAddresses,
1104
- ipAddressVersion: "IPV4"
1105
- });
1106
- priorities.push(1);
1107
- rules.push({
1108
- name: "IpAddressRule",
1109
- priority: priorities.length,
1110
- statement: {
1111
- ipSetReferenceStatement: {
1112
- arn: ipSet.attrArn
1113
- }
1114
- },
1115
- action: {
1116
- block: {}
1117
- },
1118
- visibilityConfig: {
1119
- sampledRequestsEnabled: true,
1120
- cloudWatchMetricsEnabled: true,
1121
- metricName: "IpAddressRule"
1122
- }
1123
- });
1124
- }
1125
- if (config10.security.firewall?.httpHeaders?.length) {
1126
- config10.security.firewall.httpHeaders.forEach((header, index) => {
1127
- priorities.push(1);
1128
- rules.push({
1129
- name: `HttpHeaderRule${index}`,
1130
- priority: priorities.length,
1131
- statement: {
1132
- byteMatchStatement: {
1133
- fieldToMatch: {
1134
- singleHeader: {
1135
- name: header
1136
- }
1137
- },
1138
- positionalConstraint: "EXACTLY",
1139
- searchString: "true",
1140
- textTransformations: [
1141
- {
1142
- priority: index,
1143
- type: "NONE"
1144
- }
1145
- ]
1146
- }
1147
- },
1148
- action: {
1149
- block: {}
1150
- },
1151
- visibilityConfig: {
1152
- sampledRequestsEnabled: true,
1153
- cloudWatchMetricsEnabled: true,
1154
- metricName: `HttpHeaderRule${index}`
1155
- }
1156
- });
1157
- });
1158
- }
1159
- return rules;
1160
- }
1161
- }
1162
-
1163
- // src/cloud/deployment.ts
1164
- import {AssetHashType as AssetHashType2, aws_s3_deployment as s3deploy} from "aws-cdk-lib";
1165
- import {config as config12} from "@stacksjs/config";
1166
- import {websiteSourceHash} from "@stacksjs/utils";
1167
-
1168
- class DeploymentStack {
1169
- privateSource;
1170
- docsSource;
1171
- websiteSource;
1172
- constructor(scope, props) {
1173
- this.privateSource = "../../private";
1174
- this.docsSource = "../docs/dist/";
1175
- this.websiteSource = config12.app.docMode === true ? this.docsSource : "../views/dist/";
1176
- new s3deploy.BucketDeployment(scope, "Website", {
1177
- sources: [s3deploy.Source.asset(this.websiteSource, {
1178
- assetHash: websiteSourceHash(),
1179
- assetHashType: AssetHashType2.CUSTOM
1180
- })],
1181
- destinationBucket: props.publicBucket,
1182
- distribution: props.cdn,
1183
- distributionPaths: ["/*"]
1184
- });
1185
- new s3deploy.BucketDeployment(scope, "PrivateFiles", {
1186
- sources: [s3deploy.Source.asset(this.privateSource)],
1187
- destinationBucket: props.privateBucket
1188
- });
1189
- }
1190
- }
1191
-
1192
- // src/cloud/jump-box.ts
1193
- import {CfnOutput as Output5, aws_ec2 as ec2, aws_iam as iam3} from "aws-cdk-lib";
1194
-
1195
- class JumpBoxStack {
1196
- jumpBox;
1197
- constructor(scope, props) {
1198
- const role = new iam3.Role(scope, "JumpBoxInstanceRole", {
1199
- assumedBy: new iam3.ServicePrincipal("ec2.amazonaws.com"),
1200
- managedPolicies: [
1201
- iam3.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
1202
- iam3.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy")
1203
- ]
1204
- });
1205
- this.jumpBox = new ec2.Instance(scope, "JumpBox", {
1206
- vpc: props.vpc,
1207
- instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
1208
- machineImage: new ec2.AmazonLinuxImage,
1209
- role,
1210
- userData: ec2.UserData.custom(`
1211
- #!/bin/bash
1212
- yum update -y
1213
- yum install -y amazon-efs-utils
1214
- yum install -y git
1215
- yum install -y https://s3.us-east-1.amazonaws.com/amazon-ssm-us-east-1/latest/linux_amd64/amazon-ssm-agent.rpm
1216
- mkdir /mnt/efs
1217
- mount -t efs ${props.fileSystem.fileSystemId}:/ /mnt/efs
1218
- git clone https://github.com/stacksjs/stacks.git /mnt/efs
1219
- `)
1220
- });
1221
- new Output5(scope, "JumpBoxInstanceId", {
1222
- value: this.jumpBox.instanceId,
1223
- description: "The ID of the EC2 instance that can be used to SSH into the Stacks Cloud."
1224
- });
1225
- }
1226
- }
1227
-
1228
- // src/cloud/file-system.ts
1229
- import {RemovalPolicy as RemovalPolicy5, aws_efs as efs} from "aws-cdk-lib";
1230
-
1231
- class FileSystemStack {
1232
- fileSystem;
1233
- accessPoint;
1234
- constructor(scope, props) {
1235
- this.fileSystem = new efs.FileSystem(scope, "FileSystem", {
1236
- fileSystemName: `${props.slug}-${props.appEnv}-efs`,
1237
- vpc: props.vpc,
1238
- removalPolicy: RemovalPolicy5.DESTROY,
1239
- lifecyclePolicy: efs.LifecyclePolicy.AFTER_7_DAYS,
1240
- performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
1241
- throughputMode: efs.ThroughputMode.BURSTING,
1242
- enableAutomaticBackups: true,
1243
- encrypted: true
1244
- });
1245
- this.accessPoint = new efs.AccessPoint(scope, "FileSystemAccessPoint", {
1246
- fileSystem: this.fileSystem,
1247
- path: "/",
1248
- posixUser: {
1249
- uid: "1000",
1250
- gid: "1000"
1251
- }
1252
- });
1253
- }
1254
- }
1255
-
1256
- // src/cloud/network.ts
1257
- import {aws_ec2 as ec22} from "aws-cdk-lib";
1258
-
1259
- class NetworkStack {
1260
- vpc;
1261
- constructor(scope, props) {
1262
- this.vpc = new ec22.Vpc(scope, "Network", {
1263
- vpcName: `${props.slug}-${props.appEnv}-vpc`,
1264
- ipAddresses: ec22.IpAddresses.cidr("10.0.0.0/16"),
1265
- maxAzs: 3,
1266
- natGateways: 0,
1267
- subnetConfiguration: [
1268
- {
1269
- name: "public-subnet-1",
1270
- subnetType: ec22.SubnetType.PUBLIC,
1271
- cidrMask: 24
1272
- },
1273
- {
1274
- name: "private-subnet-1",
1275
- subnetType: ec22.SubnetType.PRIVATE_ISOLATED,
1276
- cidrMask: 28
1277
- }
1278
- ]
1279
- });
1280
- }
1281
- }
1282
-
1283
- // src/cloud/redirects.ts
1284
- import {config as config14} from "@stacksjs/config";
1285
- import {RemovalPolicy as RemovalPolicy6, aws_route53 as route533, aws_s3 as s33} from "aws-cdk-lib";
1286
-
1287
- class RedirectsStack {
1288
- redirectZones = [];
1289
- constructor(scope, props) {
1290
- config14.dns.redirects?.forEach((redirect) => {
1291
- const slug2 = redirect.split(".").map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
1292
- const hostedZone = route533.HostedZone.fromLookup(scope, "HostedZone", { domainName: redirect });
1293
- const redirectBucket = new s33.Bucket(scope, `RedirectBucket${slug2}`, {
1294
- bucketName: `${redirect}-redirect`,
1295
- websiteRedirect: {
1296
- hostName: props.domain,
1297
- protocol: s33.RedirectProtocol.HTTPS
1298
- },
1299
- removalPolicy: RemovalPolicy6.DESTROY,
1300
- autoDeleteObjects: true
1301
- });
1302
- new route533.CnameRecord(scope, `RedirectRecord${slug2}`, {
1303
- zone: hostedZone,
1304
- recordName: "redirect",
1305
- domainName: redirectBucket.bucketWebsiteDomainName
1306
- });
1307
- });
1308
- config14.dns.redirects?.forEach((redirect) => {
1309
- const slug2 = redirect.split(".").map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
1310
- const hostedZone = route533.HostedZone.fromLookup(scope, `RedirectHostedZone${slug2}`, { domainName: redirect });
1311
- this.redirectZones.push(hostedZone);
1312
- });
1313
- }
1314
- }
1315
-
1316
- // src/cloud/email.ts
1317
- import {Duration as Duration5, RemovalPolicy as RemovalPolicy7, Stack, Tags as Tags3, aws_iam as iam4, aws_lambda as lambda4, aws_route53 as route534, aws_s3 as s34, aws_s3_notifications as s3n, aws_ses as ses} from "aws-cdk-lib";
1318
- import {config as config16} from "@stacksjs/config";
1319
-
1320
- class EmailStack {
1321
- emailBucket;
1322
- constructor(scope, props) {
1323
- const bucketPrefix = `${props.slug}-${props.appEnv}`;
1324
- this.emailBucket = new s34.Bucket(scope, "EmailBucket", {
1325
- bucketName: `${bucketPrefix}-email-${props.timestamp}`,
1326
- versioned: true,
1327
- removalPolicy: RemovalPolicy7.DESTROY,
1328
- autoDeleteObjects: true,
1329
- encryption: s34.BucketEncryption.S3_MANAGED,
1330
- lifecycleRules: [
1331
- {
1332
- id: "24h",
1333
- enabled: true,
1334
- expiration: Duration5.days(1),
1335
- noncurrentVersionExpiration: Duration5.days(1),
1336
- prefix: "today/"
1337
- },
1338
- {
1339
- id: "Intelligent transition for Inbox",
1340
- enabled: true,
1341
- prefix: "inbox/",
1342
- transitions: [
1343
- {
1344
- storageClass: s34.StorageClass.INTELLIGENT_TIERING,
1345
- transitionAfter: Duration5.days(0)
1346
- }
1347
- ]
1348
- },
1349
- {
1350
- id: "Intelligent transition for Sent",
1351
- enabled: true,
1352
- prefix: "sent/",
1353
- transitions: [
1354
- {
1355
- storageClass: s34.StorageClass.INTELLIGENT_TIERING,
1356
- transitionAfter: Duration5.days(0)
1357
- }
1358
- ]
1359
- }
1360
- ]
1361
- });
1362
- Tags3.of(this.emailBucket).add("daily-backup", "true");
1363
- const sesPrincipal = new iam4.ServicePrincipal("ses.amazonaws.com");
1237
+ Tags.of(this.emailBucket).add("daily-backup", "true");
1238
+ const sesPrincipal = new iam2.ServicePrincipal("ses.amazonaws.com");
1364
1239
  const ruleSetName = `${props.slug}-${props.appEnv}-email-receipt-rule-set`;
1365
1240
  const receiptRuleName = `${props.slug}-${props.appEnv}-email-receipt-rule`;
1366
1241
  const ruleSet = new ses.CfnReceiptRuleSet(scope, "SESReceiptRuleSet", {
1367
1242
  ruleSetName
1368
1243
  });
1369
- this.emailBucket.addToResourcePolicy(new iam4.PolicyStatement({
1244
+ this.emailBucket.addToResourcePolicy(new iam2.PolicyStatement({
1370
1245
  sid: "AllowSESPuts",
1371
- effect: iam4.Effect.ALLOW,
1246
+ effect: iam2.Effect.ALLOW,
1372
1247
  principals: [sesPrincipal],
1373
- actions: [
1374
- "s3:PutObject"
1375
- ],
1376
- resources: [
1377
- `${this.emailBucket.bucketArn}/*`
1378
- ],
1248
+ actions: ["s3:PutObject"],
1249
+ resources: [`${this.emailBucket.bucketArn}/*`],
1379
1250
  conditions: {
1380
1251
  StringEquals: {
1381
1252
  "aws:SourceAccount": Stack.of(scope).account
@@ -1398,22 +1269,22 @@ class EmailStack {
1398
1269
  }
1399
1270
  }
1400
1271
  ],
1401
- recipients: config16.email.mailboxes || [],
1402
- scanEnabled: config16.email.server?.scan || true,
1272
+ recipients: config12.email.mailboxes || [],
1273
+ scanEnabled: config12.email.server?.scan || true,
1403
1274
  tlsPolicy: "Require"
1404
1275
  }
1405
1276
  });
1406
1277
  receiptRule.node.addDependency(this.emailBucket);
1407
- const iamGroup = new iam4.Group(scope, "IAMGroup", {
1278
+ const iamGroup = new iam2.Group(scope, "IAMGroup", {
1408
1279
  groupName: `${props.slug}-${props.appEnv}-email-management-s3-group`
1409
1280
  });
1410
- const listBucketsPolicyStatement = new iam4.PolicyStatement({
1411
- effect: iam4.Effect.ALLOW,
1281
+ const listBucketsPolicyStatement = new iam2.PolicyStatement({
1282
+ effect: iam2.Effect.ALLOW,
1412
1283
  actions: ["s3:ListAllMyBuckets"],
1413
1284
  resources: ["*"]
1414
1285
  });
1415
- const policyStatement = new iam4.PolicyStatement({
1416
- effect: iam4.Effect.ALLOW,
1286
+ const policyStatement = new iam2.PolicyStatement({
1287
+ effect: iam2.Effect.ALLOW,
1417
1288
  actions: [
1418
1289
  "s3:ListBucket",
1419
1290
  "s3:GetObject",
@@ -1424,12 +1295,9 @@ class EmailStack {
1424
1295
  "s3:PutObjectAcl",
1425
1296
  "s3:PutObjectVersionAcl"
1426
1297
  ],
1427
- resources: [
1428
- this.emailBucket.bucketArn,
1429
- `${this.emailBucket.bucketArn}/*`
1430
- ]
1298
+ resources: [this.emailBucket.bucketArn, `${this.emailBucket.bucketArn}/*`]
1431
1299
  });
1432
- const policy = new iam4.Policy(scope, "EmailAccessPolicy", {
1300
+ const policy = new iam2.Policy(scope, "EmailAccessPolicy", {
1433
1301
  policyName: `${props.slug}-${props.appEnv}-email-management-s3-policy`,
1434
1302
  statements: [policyStatement, listBucketsPolicyStatement]
1435
1303
  });
@@ -1474,10 +1342,12 @@ class EmailStack {
1474
1342
  new route534.MxRecord(scope, "MxRecord", {
1475
1343
  zone: props.zone,
1476
1344
  recordName: "mail",
1477
- values: [{
1478
- priority: 10,
1479
- hostName: "feedback-smtp.us-east-1.amazonses.com"
1480
- }]
1345
+ values: [
1346
+ {
1347
+ priority: 10,
1348
+ hostName: "feedback-smtp.us-east-1.amazonses.com"
1349
+ }
1350
+ ]
1481
1351
  });
1482
1352
  new route534.TxtRecord(scope, "TxtSpfRecord", {
1483
1353
  zone: props.zone,
@@ -1489,12 +1359,10 @@ class EmailStack {
1489
1359
  recordName: "_dmarc",
1490
1360
  values: [`v=DMARC1;p=quarantine;pct=25;rua=mailto:dmarcreports@${props.domain}`]
1491
1361
  });
1492
- const lambdaEmailOutboundRole = new iam4.Role(scope, "LambdaEmailOutboundRole", {
1362
+ const lambdaEmailOutboundRole = new iam2.Role(scope, "LambdaEmailOutboundRole", {
1493
1363
  roleName: `${props.slug}-${props.appEnv}-email-outbound`,
1494
- assumedBy: new iam4.ServicePrincipal("lambda.amazonaws.com"),
1495
- managedPolicies: [
1496
- iam4.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
1497
- ]
1364
+ assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
1365
+ managedPolicies: [iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")]
1498
1366
  });
1499
1367
  const lambdaEmailOutbound = new lambda4.Function(scope, "LambdaEmailOutbound", {
1500
1368
  functionName: `${props.slug}-${props.appEnv}-email-outbound`,
@@ -1510,18 +1378,16 @@ class EmailStack {
1510
1378
  role: lambdaEmailOutboundRole
1511
1379
  });
1512
1380
  lambdaEmailOutboundRole.addToPolicy(policyStatement);
1513
- const sesPolicyStatement = new iam4.PolicyStatement({
1514
- effect: iam4.Effect.ALLOW,
1381
+ const sesPolicyStatement = new iam2.PolicyStatement({
1382
+ effect: iam2.Effect.ALLOW,
1515
1383
  actions: ["ses:SendRawEmail"],
1516
1384
  resources: ["*"]
1517
1385
  });
1518
1386
  lambdaEmailOutboundRole.addToPolicy(sesPolicyStatement);
1519
- const lambdaEmailInboundRole = new iam4.Role(scope, "LambdaEmailInboundRole", {
1387
+ const lambdaEmailInboundRole = new iam2.Role(scope, "LambdaEmailInboundRole", {
1520
1388
  roleName: `${props.slug}-${props.appEnv}-email-inbound`,
1521
- assumedBy: new iam4.ServicePrincipal("lambda.amazonaws.com"),
1522
- managedPolicies: [
1523
- iam4.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
1524
- ]
1389
+ assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
1390
+ managedPolicies: [iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")]
1525
1391
  });
1526
1392
  const lambdaEmailInbound = new lambda4.Function(scope, "LambdaEmailInbound", {
1527
1393
  functionName: `${props.slug}-${props.appEnv}-email-inbound`,
@@ -1541,27 +1407,22 @@ class EmailStack {
1541
1407
  functionName: lambdaEmailInbound.functionName,
1542
1408
  principal: "s3.amazonaws.com"
1543
1409
  });
1544
- const inboundS3PolicyStatement = new iam4.PolicyStatement({
1545
- effect: iam4.Effect.ALLOW,
1410
+ const inboundS3PolicyStatement = new iam2.PolicyStatement({
1411
+ effect: iam2.Effect.ALLOW,
1546
1412
  actions: ["s3:*"],
1547
- resources: [
1548
- this.emailBucket.bucketArn,
1549
- `${this.emailBucket.bucketArn}/*`
1550
- ]
1413
+ resources: [this.emailBucket.bucketArn, `${this.emailBucket.bucketArn}/*`]
1551
1414
  });
1552
1415
  lambdaEmailInboundRole.addToPolicy(inboundS3PolicyStatement);
1553
- const sesInboundPolicyStatement = new iam4.PolicyStatement({
1554
- effect: iam4.Effect.ALLOW,
1416
+ const sesInboundPolicyStatement = new iam2.PolicyStatement({
1417
+ effect: iam2.Effect.ALLOW,
1555
1418
  actions: ["ses:ListIdentities"],
1556
1419
  resources: ["*"]
1557
1420
  });
1558
1421
  lambdaEmailInboundRole.addToPolicy(sesInboundPolicyStatement);
1559
- const lambdaEmailConverterRole = new iam4.Role(scope, "LambdaEmailConverterRole", {
1422
+ const lambdaEmailConverterRole = new iam2.Role(scope, "LambdaEmailConverterRole", {
1560
1423
  roleName: `${props.slug}-${props.appEnv}-email-converter`,
1561
- assumedBy: new iam4.ServicePrincipal("lambda.amazonaws.com"),
1562
- managedPolicies: [
1563
- iam4.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
1564
- ]
1424
+ assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
1425
+ managedPolicies: [iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")]
1565
1426
  });
1566
1427
  const lambdaEmailConverter = new lambda4.Function(scope, "LambdaEmailConverter", {
1567
1428
  functionName: `${props.slug}-${props.appEnv}-email-converter`,
@@ -1576,13 +1437,10 @@ class EmailStack {
1576
1437
  BUCKET: this.emailBucket.bucketName
1577
1438
  }
1578
1439
  });
1579
- const converterS3PolicyStatement = new iam4.PolicyStatement({
1580
- effect: iam4.Effect.ALLOW,
1440
+ const converterS3PolicyStatement = new iam2.PolicyStatement({
1441
+ effect: iam2.Effect.ALLOW,
1581
1442
  actions: ["s3:*"],
1582
- resources: [
1583
- this.emailBucket.bucketArn,
1584
- `${this.emailBucket.bucketArn}/*`
1585
- ]
1443
+ resources: [this.emailBucket.bucketArn, `${this.emailBucket.bucketArn}/*`]
1586
1444
  });
1587
1445
  new lambda4.CfnPermission(scope, "S3ConverterPermission", {
1588
1446
  action: "lambda:InvokeFunction",
@@ -1590,211 +1448,134 @@ class EmailStack {
1590
1448
  principal: "s3.amazonaws.com"
1591
1449
  });
1592
1450
  lambdaEmailConverterRole.addToPolicy(converterS3PolicyStatement);
1593
- this.emailBucket.addEventNotification(s34.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(lambdaEmailInbound), { prefix: "tmp/email_in/" });
1594
- this.emailBucket.addEventNotification(s34.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(lambdaEmailOutbound), { prefix: "tmp/email_out/json/" });
1595
- this.emailBucket.addEventNotification(s34.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: "sent/" });
1596
- this.emailBucket.addEventNotification(s34.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: "inbox/" });
1597
- this.emailBucket.addEventNotification(s34.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: "today/" });
1451
+ this.emailBucket.addEventNotification(s32.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(lambdaEmailInbound), { prefix: "tmp/email_in/" });
1452
+ this.emailBucket.addEventNotification(s32.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(lambdaEmailOutbound), { prefix: "tmp/email_out/json/" });
1453
+ this.emailBucket.addEventNotification(s32.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: "sent/" });
1454
+ this.emailBucket.addEventNotification(s32.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: "inbox/" });
1455
+ this.emailBucket.addEventNotification(s32.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: "today/" });
1598
1456
  }
1599
1457
  }
1600
1458
 
1601
- // src/cloud/permissions.ts
1602
- import {SecretValue, aws_iam as iam5} from "aws-cdk-lib";
1603
- import {config as config18} from "@stacksjs/config";
1604
- import {string} from "@stacksjs/strings";
1605
- import {env as env4} from "@stacksjs/env";
1459
+ // src/cloud/file-system.ts
1460
+ import {RemovalPolicy as RemovalPolicy5, aws_efs as efs} from "aws-cdk-lib";
1606
1461
 
1607
- class PermissionsStack {
1608
- constructor(scope) {
1609
- const teamName = config18.team.name;
1610
- const users = config18.team.members;
1611
- const password = env4.AWS_DEFAULT_PASSWORD || string.random();
1612
- for (const name in users) {
1613
- const id = `User${string.pascalCase(teamName)}${string.pascalCase(name)}`;
1614
- const userName = string.slug(`${teamName}-${name}`);
1615
- const user = new iam5.User(scope, id, {
1616
- userName,
1617
- password: SecretValue.unsafePlainText(password),
1618
- passwordResetRequired: true
1619
- });
1620
- user.addManagedPolicy(iam5.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess"));
1621
- }
1462
+ class FileSystemStack {
1463
+ fileSystem;
1464
+ accessPoint;
1465
+ constructor(scope, props) {
1466
+ this.fileSystem = new efs.FileSystem(scope, "FileSystem", {
1467
+ fileSystemName: `${props.slug}-${props.appEnv}-efs`,
1468
+ vpc: props.vpc,
1469
+ removalPolicy: RemovalPolicy5.DESTROY,
1470
+ lifecyclePolicy: efs.LifecyclePolicy.AFTER_7_DAYS,
1471
+ performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
1472
+ throughputMode: efs.ThroughputMode.BURSTING,
1473
+ enableAutomaticBackups: true,
1474
+ encrypted: true
1475
+ });
1476
+ this.accessPoint = new efs.AccessPoint(scope, "FileSystemAccessPoint", {
1477
+ fileSystem: this.fileSystem,
1478
+ path: "/",
1479
+ posixUser: {
1480
+ uid: "1000",
1481
+ gid: "1000"
1482
+ }
1483
+ });
1622
1484
  }
1623
1485
  }
1624
1486
 
1625
- // src/cloud/compute.ts
1626
- import {Duration as Duration6, CfnOutput as Output6, RemovalPolicy as RemovalPolicy8, aws_ec2 as ec23, aws_ecs as ecs, aws_route53 as route535, aws_route53_targets as route53Targets, aws_secretsmanager as secretsmanager} from "aws-cdk-lib";
1627
- import {path as p4} from "@stacksjs/path";
1628
- import {env as env6} from "@stacksjs/env";
1629
- import {LogGroup} from "aws-cdk-lib/aws-logs";
1630
- import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
1487
+ // src/cloud/jump-box.ts
1488
+ import {CfnOutput as Output6, aws_ec2 as ec22, aws_iam as iam3} from "aws-cdk-lib";
1631
1489
 
1632
- class ComputeStack {
1633
- lb;
1634
- cluster;
1635
- taskDefinition;
1490
+ class JumpBoxStack {
1491
+ jumpBox;
1636
1492
  constructor(scope, props) {
1637
- const vpc = props.vpc;
1638
- const fileSystem = props.fileSystem;
1639
- if (!fileSystem)
1640
- throw new Error("The file system is missing. Please make sure it was created properly.");
1641
- this.cluster = new ecs.Cluster(scope, "StacksCluster", {
1642
- clusterName: `${props.slug}-${props.appEnv}-web-server-cluster`,
1643
- vpc
1644
- });
1645
- this.taskDefinition = new ecs.FargateTaskDefinition(scope, "TaskDefinition", {
1646
- family: `${props.appName}-${props.appEnv}-api`,
1647
- memoryLimitMiB: 512,
1648
- cpu: 256,
1649
- runtimePlatform: {
1650
- cpuArchitecture: ecs.CpuArchitecture.ARM64
1651
- }
1652
- });
1653
- const container = this.taskDefinition.addContainer("WebServerContainer", {
1654
- containerName: `${props.appName}-${props.appEnv}-api`,
1655
- image: ecs.ContainerImage.fromAsset(p4.frameworkPath("server")),
1656
- logging: new ecs.AwsLogDriver({
1657
- streamPrefix: `${props.appName}-${props.appEnv}-web-server-logs`,
1658
- logGroup: new LogGroup(scope, "StacksApiLogs", {
1659
- logGroupName: "/aws/ecs/stacks-api",
1660
- removalPolicy: RemovalPolicy8.DESTROY
1661
- })
1662
- }),
1663
- healthCheck: {
1664
- command: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"],
1665
- interval: Duration6.seconds(10),
1666
- timeout: Duration6.seconds(5),
1667
- retries: 3,
1668
- startPeriod: Duration6.seconds(10)
1669
- }
1670
- });
1671
- container.addPortMappings({
1672
- containerPort: 3000,
1673
- hostPort: 3000
1493
+ const role = new iam3.Role(scope, "JumpBoxInstanceRole", {
1494
+ assumedBy: new iam3.ServicePrincipal("ec2.amazonaws.com"),
1495
+ managedPolicies: [
1496
+ iam3.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
1497
+ iam3.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy")
1498
+ ]
1674
1499
  });
1675
- const serviceSecurityGroup = new ec23.SecurityGroup(scope, "ServiceSecurityGroup", {
1676
- securityGroupName: `${props.appName}-${props.appEnv}-api-service-sg`,
1677
- vpc,
1678
- description: "Stacks Security Group for API Service"
1500
+ this.jumpBox = new ec22.Instance(scope, "JumpBox", {
1501
+ vpc: props.vpc,
1502
+ instanceType: ec22.InstanceType.of(ec22.InstanceClass.T2, ec22.InstanceSize.MICRO),
1503
+ machineImage: new ec22.AmazonLinuxImage,
1504
+ role,
1505
+ userData: ec22.UserData.custom(`
1506
+ #!/bin/bash
1507
+ yum update -y
1508
+ yum install -y amazon-efs-utils
1509
+ yum install -y git
1510
+ yum install -y https://s3.us-east-1.amazonaws.com/amazon-ssm-us-east-1/latest/linux_amd64/amazon-ssm-agent.rpm
1511
+ mkdir /mnt/efs
1512
+ mount -t efs ${props.fileSystem.fileSystemId}:/ /mnt/efs
1513
+ git clone https://github.com/stacksjs/stacks.git /mnt/efs
1514
+ `)
1679
1515
  });
1680
- const publicLoadBalancerSG = new ec23.SecurityGroup(scope, "PublicLoadBalancerSG", {
1681
- securityGroupName: `${props.appName}-${props.appEnv}-public-load-balancer-sg`,
1682
- vpc,
1683
- description: "Access to the public facing load balancer"
1516
+ new Output6(scope, "JumpBoxInstanceId", {
1517
+ value: this.jumpBox.instanceId,
1518
+ description: "The ID of the EC2 instance that can be used to SSH into the Stacks Cloud."
1684
1519
  });
1685
- serviceSecurityGroup.addIngressRule(publicLoadBalancerSG, ec23.Port.allTraffic(), "Ingress from the public ALB");
1686
- this.lb = new elbv2.ApplicationLoadBalancer(scope, "ApplicationLoadBalancer", {
1687
- http2Enabled: true,
1688
- loadBalancerName: `${props.appName}-${props.appEnv}-alb`,
1689
- vpc,
1690
- vpcSubnets: {
1691
- subnets: vpc.selectSubnets({
1520
+ }
1521
+ }
1522
+
1523
+ // src/cloud/network.ts
1524
+ import {aws_ec2 as ec23} from "aws-cdk-lib";
1525
+
1526
+ class NetworkStack {
1527
+ vpc;
1528
+ constructor(scope, props) {
1529
+ this.vpc = new ec23.Vpc(scope, "Network", {
1530
+ vpcName: `${props.slug}-${props.appEnv}-vpc`,
1531
+ ipAddresses: ec23.IpAddresses.cidr("10.0.0.0/16"),
1532
+ maxAzs: 3,
1533
+ natGateways: 0,
1534
+ subnetConfiguration: [
1535
+ {
1536
+ name: "public-subnet-1",
1692
1537
  subnetType: ec23.SubnetType.PUBLIC,
1693
- onePerAz: true
1694
- }).subnets
1695
- },
1696
- internetFacing: true,
1697
- idleTimeout: Duration6.seconds(30),
1698
- securityGroup: publicLoadBalancerSG
1699
- });
1700
- new route535.ARecord(scope, "ApiDomainAliasRecord", {
1701
- zone: props.zone,
1702
- recordName: "api",
1703
- target: route535.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(this.lb))
1704
- });
1705
- const serviceTargetGroup = new elbv2.ApplicationTargetGroup(scope, "ServiceTargetGroup", {
1706
- targetGroupName: `${props.appName}-${props.appEnv}-api-tg`,
1707
- vpc,
1708
- targetType: elbv2.TargetType.IP,
1709
- protocol: elbv2.ApplicationProtocol.HTTP,
1710
- port: 3000,
1711
- healthCheck: {
1712
- interval: Duration6.seconds(6),
1713
- path: "/api/health",
1714
- protocol: elbv2.Protocol.HTTP,
1715
- timeout: Duration6.seconds(5),
1716
- healthyThresholdCount: 2,
1717
- unhealthyThresholdCount: 10
1718
- }
1719
- });
1720
- const service = new ecs.FargateService(scope, "StacksApiService", {
1721
- serviceName: `${props.appName}-${props.appEnv}-api-service`,
1722
- cluster: this.cluster,
1723
- taskDefinition: this.taskDefinition,
1724
- desiredCount: 1,
1725
- assignPublicIp: true,
1726
- maxHealthyPercent: 200,
1727
- vpcSubnets: vpc.selectSubnets({
1728
- subnetType: ec23.SubnetType.PUBLIC,
1729
- onePerAz: true
1730
- }),
1731
- minHealthyPercent: 75,
1732
- securityGroups: [serviceSecurityGroup]
1733
- });
1734
- service.attachToApplicationTargetGroup(serviceTargetGroup);
1735
- publicLoadBalancerSG.addIngressRule(ec23.Peer.anyIpv4(), ec23.Port.allTraffic());
1736
- this.lb.addListener("HttpsListener", {
1737
- port: 443,
1738
- certificates: [props.certificate],
1739
- defaultAction: elbv2.ListenerAction.forward([serviceTargetGroup])
1740
- });
1741
- this.lb.addListener("HttpListener", {
1742
- port: 80,
1743
- defaultAction: elbv2.ListenerAction.forward([serviceTargetGroup])
1744
- });
1745
- props.fileSystem.connections.allowFromAnyIpv4(ec23.Port.tcp(2049));
1746
- const volumeName = `${props.slug}-${props.appEnv}-efs`;
1747
- this.taskDefinition.addVolume({
1748
- name: volumeName,
1749
- efsVolumeConfiguration: {
1750
- fileSystemId: props.fileSystem.fileSystemId
1751
- }
1752
- });
1753
- container.addMountPoints({
1754
- sourceVolume: volumeName,
1755
- containerPath: "/mnt/efs",
1756
- readOnly: false
1757
- });
1758
- const scaling = service.autoScaleTaskCount({ maxCapacity: 2 });
1759
- scaling.scaleOnCpuUtilization("CpuScaling", {
1760
- targetUtilizationPercent: 50,
1761
- scaleInCooldown: Duration6.seconds(60),
1762
- scaleOutCooldown: Duration6.seconds(60)
1763
- });
1764
- scaling.scaleOnMemoryUtilization("MemoryScaling", {
1765
- targetUtilizationPercent: 60,
1766
- scaleInCooldown: Duration6.seconds(60),
1767
- scaleOutCooldown: Duration6.seconds(60)
1768
- });
1769
- const keysToRemove = ["_HANDLER", "_X_AMZN_TRACE_ID", "AWS_REGION", "AWS_EXECUTION_ENV", "AWS_LAMBDA_FUNCTION_NAME", "AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "AWS_LAMBDA_FUNCTION_VERSION", "AWS_LAMBDA_INITIALIZATION_TYPE", "AWS_LAMBDA_LOG_GROUP_NAME", "AWS_LAMBDA_LOG_STREAM_NAME", "AWS_ACCESS_KEY", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_LAMBDA_RUNTIME_API", "LAMBDA_TASK_ROOT", "LAMBDA_RUNTIME_DIR", "_"];
1770
- keysToRemove.forEach((key) => delete env6[key]);
1771
- const secrets = new secretsmanager.Secret(scope, "StacksSecrets", {
1772
- secretName: `${props.slug}-${props.appEnv}-secrets`,
1773
- description: "Secrets for the Stacks application",
1774
- generateSecretString: {
1775
- secretStringTemplate: JSON.stringify(env6),
1776
- generateStringKey: Object.keys(env6).join(",").length.toString()
1777
- }
1778
- });
1779
- secrets.grantRead(service.taskDefinition.executionRole);
1780
- container.addEnvironment("SECRETS_ARN", secrets.secretArn);
1781
- const apiPrefix = "api";
1782
- new Output6(scope, "ApiUrl", {
1783
- value: `https://${props.domain}/${apiPrefix}`,
1784
- description: "The URL of the deployed application"
1785
- });
1786
- new Output6(scope, "ApiVanityUrl", {
1787
- value: `http://${this.lb.loadBalancerDnsName}`,
1788
- description: "The Vanity URL / DNS name of the load balancer"
1538
+ cidrMask: 24
1539
+ },
1540
+ {
1541
+ name: "private-subnet-1",
1542
+ subnetType: ec23.SubnetType.PRIVATE_ISOLATED,
1543
+ cidrMask: 28
1544
+ }
1545
+ ]
1789
1546
  });
1790
1547
  }
1791
1548
  }
1792
1549
 
1550
+ // src/cloud/permissions.ts
1551
+ import {config as config14} from "@stacksjs/config";
1552
+ import {env as env6} from "@stacksjs/env";
1553
+ import {string} from "@stacksjs/strings";
1554
+ import {SecretValue, aws_iam as iam4} from "aws-cdk-lib";
1555
+
1556
+ class PermissionsStack {
1557
+ constructor(scope) {
1558
+ const teamName = config14.team.name;
1559
+ const users = config14.team.members;
1560
+ const password = env6.AWS_DEFAULT_PASSWORD || string.random();
1561
+ for (const name in users) {
1562
+ const id = `User${string.pascalCase(teamName)}${string.pascalCase(name)}`;
1563
+ const userName = string.slug(`${teamName}-${name}`);
1564
+ const user = new iam4.User(scope, id, {
1565
+ userName,
1566
+ password: SecretValue.unsafePlainText(password),
1567
+ passwordResetRequired: true
1568
+ });
1569
+ user.addManagedPolicy(iam4.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess"));
1570
+ }
1571
+ }
1572
+ }
1573
+
1793
1574
  // src/cloud/queue.ts
1794
- import {aws_ec2 as ec24} from "aws-cdk-lib";
1795
- import {pascalCase, slug as slug2} from "@stacksjs/strings";
1796
- import {fs} from "@stacksjs/storage";
1797
1575
  import {path as path6} from "@stacksjs/path";
1576
+ import {fs} from "@stacksjs/storage";
1577
+ import {pascalCase, slug as slug2} from "@stacksjs/strings";
1578
+ import {aws_ec2 as ec24} from "aws-cdk-lib";
1798
1579
  import {Rule, Schedule} from "aws-cdk-lib/aws-events";
1799
1580
  import {EcsTask} from "aws-cdk-lib/aws-events-targets";
1800
1581
 
@@ -1808,8 +1589,10 @@ class QueueStack {
1808
1589
  async init() {
1809
1590
  const jobsDir = path6.jobsPath();
1810
1591
  const actionsDir = path6.appPath("Actions");
1592
+ const ormActionDir = path6.projectStoragePath("framework/orm/Actions");
1811
1593
  const jobFiles = await fs.readdir(jobsDir);
1812
1594
  const actionFiles = await fs.readdir(actionsDir);
1595
+ const ormActionFiles = await fs.readdir(ormActionDir);
1813
1596
  const jobs = [];
1814
1597
  for (const file of jobFiles) {
1815
1598
  if (!file.endsWith(".ts"))
@@ -1819,10 +1602,19 @@ class QueueStack {
1819
1602
  this.createQueueRule(job, file);
1820
1603
  jobs.push(job);
1821
1604
  }
1605
+ for (const ormFile of ormActionFiles) {
1606
+ if (!ormFile.endsWith(".ts"))
1607
+ continue;
1608
+ const ormActionPath = path6.projectStoragePath(`framework/orm/Actions/${ormFile}`);
1609
+ const ormAction = await this.loadModule(ormActionPath);
1610
+ this.createQueueRule(ormAction, ormFile);
1611
+ jobs.push(ormAction);
1612
+ }
1822
1613
  for (const file of actionFiles) {
1823
1614
  if (!file.endsWith(".ts"))
1824
1615
  continue;
1825
1616
  const actionPath = path6.appPath(`Actions/${file}`);
1617
+ console.log(actionPath);
1826
1618
  const action = await this.loadModule(actionPath);
1827
1619
  this.createQueueRule(action, file);
1828
1620
  jobs.push(action);
@@ -1894,6 +1686,303 @@ class QueueStack {
1894
1686
  }
1895
1687
  }
1896
1688
 
1689
+ // src/cloud/redirects.ts
1690
+ import {config as config16} from "@stacksjs/config";
1691
+ import {RemovalPolicy as RemovalPolicy6, aws_route53 as route535, aws_s3 as s33} from "aws-cdk-lib";
1692
+
1693
+ class RedirectsStack {
1694
+ redirectZones = [];
1695
+ constructor(scope, props) {
1696
+ config16.dns.redirects?.forEach((redirect) => {
1697
+ const slug3 = redirect.split(".").map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
1698
+ const hostedZone = route535.HostedZone.fromLookup(scope, "HostedZone", {
1699
+ domainName: redirect
1700
+ });
1701
+ const redirectBucket = new s33.Bucket(scope, `RedirectBucket${slug3}`, {
1702
+ bucketName: `${redirect}-redirect`,
1703
+ websiteRedirect: {
1704
+ hostName: props.domain,
1705
+ protocol: s33.RedirectProtocol.HTTPS
1706
+ },
1707
+ removalPolicy: RemovalPolicy6.DESTROY,
1708
+ autoDeleteObjects: true
1709
+ });
1710
+ new route535.CnameRecord(scope, `RedirectRecord${slug3}`, {
1711
+ zone: hostedZone,
1712
+ recordName: "redirect",
1713
+ domainName: redirectBucket.bucketWebsiteDomainName
1714
+ });
1715
+ });
1716
+ config16.dns.redirects?.forEach((redirect) => {
1717
+ const slug3 = redirect.split(".").map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
1718
+ const hostedZone = route535.HostedZone.fromLookup(scope, `RedirectHostedZone${slug3}`, { domainName: redirect });
1719
+ this.redirectZones.push(hostedZone);
1720
+ });
1721
+ }
1722
+ }
1723
+
1724
+ // src/cloud/security.ts
1725
+ import {config as config18} from "@stacksjs/config";
1726
+ import {
1727
+ Duration as Duration6,
1728
+ RemovalPolicy as RemovalPolicy7,
1729
+ Tags as Tags2,
1730
+ aws_certificatemanager as acm,
1731
+ aws_kms as kms,
1732
+ aws_wafv2 as wafv2
1733
+ } from "aws-cdk-lib";
1734
+
1735
+ class SecurityStack {
1736
+ firewall;
1737
+ kmsKey;
1738
+ certificate;
1739
+ constructor(scope, props) {
1740
+ const firewallOptions = config18.cloud.firewall;
1741
+ if (!firewallOptions)
1742
+ throw new Error("No firewall options found in config");
1743
+ const options = {
1744
+ defaultAction: { allow: {} },
1745
+ scope: "CLOUDFRONT",
1746
+ visibilityConfig: {
1747
+ sampledRequestsEnabled: true,
1748
+ cloudWatchMetricsEnabled: true,
1749
+ metricName: "firewallMetric"
1750
+ },
1751
+ rules: this.getFirewallRules(scope)
1752
+ };
1753
+ this.firewall = new wafv2.CfnWebACL(scope, "StacksWebFirewall", options);
1754
+ Tags2.of(this.firewall).add("Name", "waf-cloudfront", { priority: 300 });
1755
+ Tags2.of(this.firewall).add("Purpose", "CloudFront", { priority: 300 });
1756
+ Tags2.of(this.firewall).add("CreatedBy", "CloudFormation", {
1757
+ priority: 300
1758
+ });
1759
+ this.kmsKey = new kms.Key(scope, "EncryptionKey", {
1760
+ alias: "stacks-encryption-key",
1761
+ description: "KMS key for Stacks Cloud",
1762
+ enableKeyRotation: true,
1763
+ removalPolicy: RemovalPolicy7.DESTROY,
1764
+ pendingWindow: Duration6.days(30)
1765
+ });
1766
+ this.certificate = new acm.Certificate(scope, "Certificate", {
1767
+ domainName: props.domain,
1768
+ validation: acm.CertificateValidation.fromDns(props.zone),
1769
+ subjectAlternativeNames: [`www.${props.domain}`, `api.${props.domain}`]
1770
+ });
1771
+ }
1772
+ getFirewallRules(scope) {
1773
+ const rules = [];
1774
+ const priorities = [];
1775
+ if (config18.security.firewall?.countryCodes?.length) {
1776
+ priorities.push(1);
1777
+ rules.push({
1778
+ name: "CountryRule",
1779
+ priority: priorities.length,
1780
+ statement: {},
1781
+ action: {
1782
+ block: {}
1783
+ },
1784
+ visibilityConfig: {
1785
+ sampledRequestsEnabled: true,
1786
+ cloudWatchMetricsEnabled: true,
1787
+ metricName: "CountryRule"
1788
+ }
1789
+ });
1790
+ }
1791
+ if (config18.security.firewall?.ipAddresses?.length) {
1792
+ const ipSet = new wafv2.CfnIPSet(scope, "IpSet", {
1793
+ name: "IpSet",
1794
+ description: "IP Set",
1795
+ scope: "CLOUDFRONT",
1796
+ addresses: config18.security.firewall.ipAddresses,
1797
+ ipAddressVersion: "IPV4"
1798
+ });
1799
+ priorities.push(1);
1800
+ rules.push({
1801
+ name: "IpAddressRule",
1802
+ priority: priorities.length,
1803
+ statement: {
1804
+ ipSetReferenceStatement: {
1805
+ arn: ipSet.attrArn
1806
+ }
1807
+ },
1808
+ action: {
1809
+ block: {}
1810
+ },
1811
+ visibilityConfig: {
1812
+ sampledRequestsEnabled: true,
1813
+ cloudWatchMetricsEnabled: true,
1814
+ metricName: "IpAddressRule"
1815
+ }
1816
+ });
1817
+ }
1818
+ if (config18.security.firewall?.httpHeaders?.length) {
1819
+ config18.security.firewall.httpHeaders.forEach((header, index) => {
1820
+ priorities.push(1);
1821
+ rules.push({
1822
+ name: `HttpHeaderRule${index}`,
1823
+ priority: priorities.length,
1824
+ statement: {
1825
+ byteMatchStatement: {
1826
+ fieldToMatch: {
1827
+ singleHeader: {
1828
+ name: header
1829
+ }
1830
+ },
1831
+ positionalConstraint: "EXACTLY",
1832
+ searchString: "true",
1833
+ textTransformations: [
1834
+ {
1835
+ priority: index,
1836
+ type: "NONE"
1837
+ }
1838
+ ]
1839
+ }
1840
+ },
1841
+ action: {
1842
+ block: {}
1843
+ },
1844
+ visibilityConfig: {
1845
+ sampledRequestsEnabled: true,
1846
+ cloudWatchMetricsEnabled: true,
1847
+ metricName: `HttpHeaderRule${index}`
1848
+ }
1849
+ });
1850
+ });
1851
+ }
1852
+ return rules;
1853
+ }
1854
+ }
1855
+
1856
+ // src/cloud/storage.ts
1857
+ import {RemovalPolicy as RemovalPolicy8, Tags as Tags3, aws_backup as backup, aws_iam as iam5, aws_s3 as s34} from "aws-cdk-lib";
1858
+
1859
+ class StorageStack {
1860
+ publicBucket;
1861
+ privateBucket;
1862
+ logBucket;
1863
+ bucketPrefix;
1864
+ vault;
1865
+ backupPlan;
1866
+ backupRole;
1867
+ constructor(scope, props) {
1868
+ this.bucketPrefix = `${props.slug}-${props.appEnv}`;
1869
+ this.publicBucket = new s34.Bucket(scope, "PublicBucket", {
1870
+ bucketName: `${this.bucketPrefix}-public-${props.timestamp}`,
1871
+ versioned: true,
1872
+ autoDeleteObjects: true,
1873
+ removalPolicy: RemovalPolicy8.DESTROY,
1874
+ encryption: s34.BucketEncryption.S3_MANAGED
1875
+ });
1876
+ Tags3.of(this.publicBucket).add("daily-backup", "true");
1877
+ this.privateBucket = new s34.Bucket(scope, "PrivateBucket", {
1878
+ bucketName: `${this.bucketPrefix}-private-${props.timestamp}`,
1879
+ versioned: true,
1880
+ removalPolicy: RemovalPolicy8.DESTROY,
1881
+ autoDeleteObjects: true,
1882
+ encryption: s34.BucketEncryption.S3_MANAGED,
1883
+ enforceSSL: true,
1884
+ publicReadAccess: false,
1885
+ blockPublicAccess: {
1886
+ blockPublicAcls: true,
1887
+ blockPublicPolicy: true,
1888
+ ignorePublicAcls: true,
1889
+ restrictPublicBuckets: true
1890
+ }
1891
+ });
1892
+ Tags3.of(this.privateBucket).add("daily-backup", "true");
1893
+ this.logBucket = new s34.Bucket(scope, "LogsBucket", {
1894
+ bucketName: `${this.bucketPrefix}-logs-${props.timestamp}`,
1895
+ removalPolicy: RemovalPolicy8.RETAIN,
1896
+ blockPublicAccess: new s34.BlockPublicAccess({
1897
+ blockPublicAcls: false,
1898
+ ignorePublicAcls: true,
1899
+ blockPublicPolicy: true,
1900
+ restrictPublicBuckets: true
1901
+ }),
1902
+ objectOwnership: s34.ObjectOwnership.BUCKET_OWNER_PREFERRED
1903
+ });
1904
+ Tags3.of(this.logBucket).add("daily-backup", "true");
1905
+ this.backupRole = this.createBackupRole(scope);
1906
+ this.vault = new backup.BackupVault(scope, "BackupVault", {
1907
+ backupVaultName: `${props.slug}-${props.appEnv}-daily-backup-vault`,
1908
+ encryptionKey: props.kmsKey,
1909
+ removalPolicy: RemovalPolicy8.DESTROY
1910
+ });
1911
+ this.backupPlan = backup.BackupPlan.daily35DayRetention(scope, "BackupPlan", this.vault);
1912
+ this.backupPlan.addSelection("Selection", {
1913
+ role: this.backupRole,
1914
+ resources: [backup.BackupResource.fromTag("daily-backup", "true")]
1915
+ });
1916
+ }
1917
+ createBackupRole(scope) {
1918
+ const backupRole = new iam5.Role(scope, "BackupRole", {
1919
+ assumedBy: new iam5.ServicePrincipal("backup.amazonaws.com")
1920
+ });
1921
+ backupRole.addToPolicy(new iam5.PolicyStatement({
1922
+ actions: [
1923
+ "s3:GetInventoryConfiguration",
1924
+ "s3:PutInventoryConfiguration",
1925
+ "s3:ListBucketVersions",
1926
+ "s3:ListBucket",
1927
+ "s3:GetBucketVersioning",
1928
+ "s3:GetBucketNotification",
1929
+ "s3:PutBucketNotification",
1930
+ "s3:GetBucketLocation",
1931
+ "s3:GetBucketTagging"
1932
+ ],
1933
+ resources: ["arn:aws:s3:::*"],
1934
+ sid: "S3BucketBackupPermissions"
1935
+ }));
1936
+ backupRole.addToPolicy(new iam5.PolicyStatement({
1937
+ actions: [
1938
+ "s3:GetObjectAcl",
1939
+ "s3:GetObject",
1940
+ "s3:GetObjectVersionTagging",
1941
+ "s3:GetObjectVersionAcl",
1942
+ "s3:GetObjectTagging",
1943
+ "s3:GetObjectVersion"
1944
+ ],
1945
+ resources: ["arn:aws:s3:::*/*"],
1946
+ sid: "S3ObjectBackupPermissions"
1947
+ }));
1948
+ backupRole.addToPolicy(new iam5.PolicyStatement({
1949
+ actions: ["s3:ListAllMyBuckets"],
1950
+ resources: ["*"],
1951
+ sid: "S3GlobalPermissions"
1952
+ }));
1953
+ backupRole.addToPolicy(new iam5.PolicyStatement({
1954
+ actions: ["kms:Decrypt", "kms:DescribeKey"],
1955
+ resources: ["*"],
1956
+ sid: "KMSBackupPermissions",
1957
+ conditions: {
1958
+ StringLike: {
1959
+ "kms:ViaService": "s3.*.amazonaws.com"
1960
+ }
1961
+ }
1962
+ }));
1963
+ backupRole.addToPolicy(new iam5.PolicyStatement({
1964
+ actions: [
1965
+ "events:DescribeRule",
1966
+ "events:EnableRule",
1967
+ "events:PutRule",
1968
+ "events:DeleteRule",
1969
+ "events:PutTargets",
1970
+ "events:RemoveTargets",
1971
+ "events:ListTargetsByRule",
1972
+ "events:DisableRule"
1973
+ ],
1974
+ resources: ["arn:aws:events:*:*:rule/AwsBackupManagedRule*"],
1975
+ sid: "EventsPermissions"
1976
+ }));
1977
+ backupRole.addToPolicy(new iam5.PolicyStatement({
1978
+ actions: ["cloudwatch:GetMetricData", "events:ListRules"],
1979
+ resources: ["*"],
1980
+ sid: "EventsMetricsGlobalPermissions"
1981
+ }));
1982
+ return backupRole;
1983
+ }
1984
+ }
1985
+
1897
1986
  // src/cloud/index.ts
1898
1987
  class Cloud extends Stack2 {
1899
1988
  dns;
@@ -1948,7 +2037,7 @@ class Cloud extends Stack2 {
1948
2037
  this.cli = new CliStack(this, props);
1949
2038
  }
1950
2039
  async init() {
1951
- if (config20.cloud.api?.deploy) {
2040
+ if (config20.api?.deploy) {
1952
2041
  const props = this.props;
1953
2042
  this.api = new ComputeStack(this, {
1954
2043
  ...props,