@stacksjs/cloud 0.58.28 → 0.58.44

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 (2) hide show
  1. package/dist/index.js +2718 -0
  2. package/package.json +33 -33
package/dist/index.js ADDED
@@ -0,0 +1,2718 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true,
8
+ configurable: true,
9
+ set: (newValue) => all[name] = () => newValue
10
+ });
11
+ };
12
+
13
+ // src/helpers.ts
14
+ import {CloudFormation} from "@aws-sdk/client-cloudformation";
15
+ import {CloudWatchLogsClient, DeleteLogGroupCommand, DescribeLogGroupsCommand} from "@aws-sdk/client-cloudwatch-logs";
16
+ import {EC2, _InstanceType as InstanceType} from "@aws-sdk/client-ec2";
17
+ import {DescribeFileSystemsCommand, EFSClient} from "@aws-sdk/client-efs";
18
+ import {IAM} from "@aws-sdk/client-iam";
19
+ import {SSM} from "@aws-sdk/client-ssm";
20
+ import {Lambda} from "@aws-sdk/client-lambda";
21
+ import {ContactType, Route53Domains} from "@aws-sdk/client-route-53-domains";
22
+ import {ListBucketsCommand, S3} from "@aws-sdk/client-s3";
23
+ import {config as config2} from "@stacksjs/config";
24
+ import {err, handleError as handleError2, ok} from "@stacksjs/error-handling";
25
+ import {log} from "@stacksjs/logging";
26
+ import {path as p} from "@stacksjs/path";
27
+ import {rimraf} from "@stacksjs/utils";
28
+ import {slug} from "@stacksjs/strings";
29
+ async function getSecurityGroupId(securityGroupName) {
30
+ const ec2 = new EC2({ region: "us-east-1" });
31
+ const { SecurityGroups } = await ec2.describeSecurityGroups({
32
+ Filters: [{ Name: "group-name", Values: [securityGroupName] }]
33
+ });
34
+ if (!SecurityGroups)
35
+ return err(`Security group ${securityGroupName} not found`);
36
+ if (SecurityGroups[0])
37
+ return ok(SecurityGroups[0].GroupId);
38
+ return err(`Security group ${securityGroupName} not found`);
39
+ }
40
+ function purchaseDomain(domain, options) {
41
+ const route53domains = new Route53Domains({ region: "us-east-1" });
42
+ const contactType = options.contactType.toUpperCase();
43
+ const params = {
44
+ DomainName: domain,
45
+ DurationInYears: options.years || 1,
46
+ AutoRenew: options.autoRenew || true,
47
+ AdminContact: {
48
+ FirstName: options.adminFirstName,
49
+ LastName: options.adminLastName,
50
+ ContactType: contactType || ContactType.PERSON,
51
+ OrganizationName: options.adminOrganization,
52
+ AddressLine1: options.adminAddressLine1,
53
+ AddressLine2: options.adminAddressLine2,
54
+ City: options.adminCity,
55
+ State: options.adminState,
56
+ CountryCode: options.adminCountry,
57
+ ZipCode: options.adminZip.toString(),
58
+ PhoneNumber: options.adminPhone.toString().includes("+") ? options.adminPhone.toString() : `+${options.adminPhone.toString()}`,
59
+ Email: options.adminEmail
60
+ },
61
+ RegistrantContact: {
62
+ FirstName: options.registrantFirstName,
63
+ LastName: options.registrantLastName,
64
+ ContactType: contactType || ContactType.PERSON,
65
+ OrganizationName: options.registrantOrganization,
66
+ AddressLine1: options.registrantAddressLine1,
67
+ AddressLine2: options.registrantAddressLine2,
68
+ City: options.registrantCity,
69
+ State: options.registrantState,
70
+ CountryCode: options.registrantCountry,
71
+ ZipCode: options.registrantZip.toString(),
72
+ PhoneNumber: options.registrantPhone.toString().includes("+") ? options.registrantPhone.toString() : `+${options.registrantPhone.toString()}`,
73
+ Email: options.registrantEmail
74
+ },
75
+ TechContact: {
76
+ FirstName: options.techFirstName,
77
+ LastName: options.techLastName,
78
+ ContactType: contactType || ContactType.PERSON,
79
+ OrganizationName: options.techOrganization,
80
+ AddressLine1: options.techAddressLine1,
81
+ AddressLine2: options.techAddressLine2,
82
+ City: options.techCity,
83
+ State: options.techState,
84
+ CountryCode: options.techCountry,
85
+ ZipCode: options.techZip.toString(),
86
+ PhoneNumber: options.techPhone.toString().includes("+") ? options.techPhone.toString() : `+${options.techPhone.toString()}`,
87
+ Email: options.techEmail
88
+ },
89
+ PrivacyProtectAdminContact: options.privacyAdmin || options.privacy || true,
90
+ PrivacyProtectRegistrantContact: options.privacyRegistrant || options.privacy || true,
91
+ PrivacyProtectTechContact: options.privacyTech || options.privacy || true
92
+ };
93
+ try {
94
+ return ok(route53domains.registerDomain(params));
95
+ } catch (error) {
96
+ return err(error);
97
+ }
98
+ }
99
+ async function getJumpBoxInstanceId(name) {
100
+ if (!name)
101
+ name = `${cloudName}/JumpBox`;
102
+ const ec2 = new EC2({ region: "us-east-1" });
103
+ const data = await ec2.describeInstances({
104
+ Filters: [
105
+ {
106
+ Name: "tag:Name",
107
+ Values: [name]
108
+ }
109
+ ]
110
+ });
111
+ if (data.Reservations && data.Reservations[0] && data.Reservations[0].Instances && data.Reservations[0].Instances[0])
112
+ return data.Reservations[0].Instances[0].InstanceId;
113
+ return;
114
+ }
115
+ async function deleteEc2Instance(id, stackName) {
116
+ if (!stackName)
117
+ stackName = cloudName;
118
+ if (!id)
119
+ return err(`Instance ${id} not found`);
120
+ const ec2 = new EC2({ region: "us-east-1" });
121
+ await ec2.terminateInstances({ InstanceIds: [id] });
122
+ return ok(`Instance ${id} is being terminated`);
123
+ }
124
+ async function deleteJumpBox(stackName) {
125
+ if (!stackName)
126
+ stackName = cloudName;
127
+ const jumpBoxId = await getJumpBoxInstanceId();
128
+ if (!jumpBoxId)
129
+ return err("Jump-box not found");
130
+ return await deleteEc2Instance(jumpBoxId, stackName);
131
+ }
132
+ async function deleteIamUsers() {
133
+ const iam = new IAM({ region: "us-east-1" });
134
+ const data = await iam.listUsers({});
135
+ const teamName = slug(config2.team.name);
136
+ const users = data.Users?.filter((user) => {
137
+ const userNameLower = user.UserName?.toLowerCase();
138
+ return userNameLower !== "stacks" && userNameLower !== teamName.toLowerCase() && userNameLower?.includes(teamName.toLowerCase());
139
+ }) || [];
140
+ if (!users || users.length === 0)
141
+ return ok(`No Stacks IAM users found for team ${teamName}`);
142
+ const promises = users.map(async (user) => {
143
+ const userName = user.UserName || "";
144
+ log.info(`Deleting IAM user: ${userName}`);
145
+ const policies = await iam.listAttachedUserPolicies({ UserName: userName });
146
+ await Promise.all(policies.AttachedPolicies?.map((policy) => iam.detachUserPolicy({ UserName: userName, PolicyArn: policy.PolicyArn || "" })) || []);
147
+ const accessKeys = await iam.listAccessKeys({ UserName: userName });
148
+ await Promise.all(accessKeys.AccessKeyMetadata?.map((key) => iam.deleteAccessKey({ UserName: userName, AccessKeyId: key.AccessKeyId || "" })) || []);
149
+ return iam.deleteUser({ UserName: userName });
150
+ });
151
+ await Promise.all(promises).catch((error) => {
152
+ console.error(`Error deleting user: ${error}`);
153
+ return err(handleError2("Error deleting Stacks IAM users", error));
154
+ });
155
+ return ok(`Stacks IAM users deleted for team ${teamName}`);
156
+ }
157
+ async function deleteStacksBuckets() {
158
+ try {
159
+ const s3 = new S3({ region: "us-east-1" });
160
+ const data = await s3.listBuckets({});
161
+ const stacksBuckets = data.Buckets?.filter((bucket) => bucket.Name?.includes("stacks"));
162
+ if (!stacksBuckets)
163
+ return err("No stacks buckets found");
164
+ const promises = stacksBuckets.map(async (bucket) => {
165
+ const bucketName = bucket.Name || "";
166
+ log.info(`Deleting bucket ${bucketName}...`);
167
+ const objects = await s3.listObjectsV2({ Bucket: bucketName });
168
+ log.info(`Finished listing bucket ${bucketName} objects`);
169
+ if (objects.Contents) {
170
+ log.info("Deleting bucket objects...");
171
+ await Promise.all(objects.Contents.map((object) => s3.deleteObject({ Bucket: bucketName, Key: object.Key || "" }).catch((error) => handleError2(error))));
172
+ log.info(`Finished deleting objects from bucket ${bucketName}`);
173
+ }
174
+ log.info(`Deleting bucket ${bucketName} versions...`);
175
+ try {
176
+ const versions = await s3.listObjectVersions({ Bucket: bucketName });
177
+ if (versions.Versions) {
178
+ await Promise.all(versions.Versions.map((version) => s3.deleteObject({ Bucket: bucketName, Key: version.Key || "", VersionId: version.VersionId }))).catch((error) => handleError2(error));
179
+ log.info(`Finished deleting versions from bucket ${bucketName}`);
180
+ }
181
+ log.info(`Deleting bucket ${bucketName} delete markers...`);
182
+ if (versions.DeleteMarkers) {
183
+ await Promise.all(versions.DeleteMarkers.map((marker) => s3.deleteObject({ Bucket: bucketName, Key: marker.Key || "", VersionId: marker.VersionId }))).catch((error) => handleError2(error));
184
+ log.info(`Finished deleting delete markers from bucket ${bucketName}`);
185
+ }
186
+ const uploads = await s3.listMultipartUploads({ Bucket: bucketName });
187
+ if (uploads.Uploads) {
188
+ log.info("Aborting bucket multipart uploads...");
189
+ await Promise.all(uploads.Uploads.map((upload) => s3.abortMultipartUpload({ Bucket: bucketName, Key: upload.Key || "", UploadId: upload.UploadId }))).catch((error) => handleError2(error));
190
+ log.info(`Finished aborting multipart uploads from bucket ${bucketName}`);
191
+ }
192
+ await s3.deleteBucket({ Bucket: bucketName }).catch((error) => handleError2(error));
193
+ log.info(`Bucket ${bucketName} deleted`);
194
+ } catch (error) {
195
+ log.info(`Error listing bucket ${bucketName} versions`, error);
196
+ }
197
+ });
198
+ await Promise.all(promises).catch((error) => {
199
+ return err(handleError2("Error deleting stacks buckets", error));
200
+ });
201
+ return ok("Stacks buckets deleted");
202
+ } catch (error) {
203
+ return err(handleError2("Error deleting stacks buckets", error));
204
+ }
205
+ }
206
+ async function deleteStacksFunctions() {
207
+ const lambda = new Lambda({ region: "us-east-1" });
208
+ const data = await lambda.listFunctions({});
209
+ const stacksFunctions = data.Functions?.filter((func) => func.FunctionName?.includes("stacks")) || [];
210
+ if (!stacksFunctions || stacksFunctions.length === 0)
211
+ return ok("No stacks functions found");
212
+ const promises = stacksFunctions.map((func) => lambda.deleteFunction({ FunctionName: func.FunctionName || "" }));
213
+ await Promise.all(promises).catch((error) => {
214
+ if (error.message.includes("it is a replicated function")) {
215
+ log.info("Function is replicated, skipping...");
216
+ return ok("CloudFront is still deleting the some functions. Try again later.");
217
+ }
218
+ return err(handleError2("Error deleting stacks functions", error));
219
+ });
220
+ return ok("Stacks functions deleted");
221
+ }
222
+ async function deleteLogGroups() {
223
+ try {
224
+ const client = new CloudWatchLogsClient({ region: "us-east-1" });
225
+ const logGroups = await client.send(new DescribeLogGroupsCommand({}));
226
+ if (!logGroups?.logGroups)
227
+ return err("No log groups found");
228
+ for (const group of logGroups.logGroups) {
229
+ if (group.logGroupName?.includes("stacks"))
230
+ await client.send(new DeleteLogGroupCommand({ logGroupName: group.logGroupName }));
231
+ }
232
+ return ok("Log groups deleted");
233
+ } catch (error) {
234
+ return err(handleError2("Error deleting log groups", error));
235
+ }
236
+ }
237
+ async function deleteParameterStore() {
238
+ const ssm = new SSM({ region: "us-east-1" });
239
+ const data = await ssm.describeParameters({});
240
+ if (!data.Parameters)
241
+ return ok("No parameters found");
242
+ const stacksParameters = data.Parameters.filter((param) => param.Name?.includes("stacks")) || [];
243
+ if (!stacksParameters || stacksParameters.length === 0)
244
+ return ok("No stacks parameters found");
245
+ const promises = stacksParameters.map((param) => ssm.deleteParameter({ Name: param.Name || "" }));
246
+ await Promise.all(promises).catch((error) => {
247
+ return err(handleError2("Error deleting parameter store", error));
248
+ });
249
+ return ok("Parameter store deleted");
250
+ }
251
+ async function deleteCdkRemnants() {
252
+ try {
253
+ return ok(await rimraf([
254
+ p.cloudPath("cdk.out/"),
255
+ p.cloudPath("cdk.context.json")
256
+ ]));
257
+ } catch (error) {
258
+ return err(handleError2("Error deleting CDK remnants", error));
259
+ }
260
+ }
261
+ async function hasBeenDeployed() {
262
+ const s3 = new S3({ region: "us-east-1" });
263
+ try {
264
+ const response = await s3.send(new ListBucketsCommand({}));
265
+ return ok(response.Buckets?.some((bucket) => bucket.Name?.includes(config2.app.name?.toLocaleLowerCase() || "stacks")) || false);
266
+ } catch (error) {
267
+ return err(handleError2("Error checking if the app has been deployed", error));
268
+ }
269
+ }
270
+ async function getJumpBoxInstanceProfileName() {
271
+ const iam = new IAM({ region: "us-east-1" });
272
+ const data = await iam.listInstanceProfiles({});
273
+ const instanceProfile = data.InstanceProfiles?.find((profile) => profile.InstanceProfileName?.includes("JumpBox"));
274
+ if (!instanceProfile)
275
+ return err("Jump-box IAM instance profile not found");
276
+ return ok(instanceProfile?.InstanceProfileName);
277
+ }
278
+ async function addJumpBox(stackName) {
279
+ if (!stackName)
280
+ stackName = cloudName;
281
+ if (await getJumpBoxInstanceId())
282
+ return err("The jump\u2013box you are trying to add already exists. Please remove it & wait until it finished terminating.");
283
+ const ec2 = new EC2({ region: "us-east-1" });
284
+ const r = await getJumpBoxSecurityGroupName();
285
+ if (r.isErr())
286
+ return err(r.error);
287
+ if (!r.value)
288
+ return err("Security group not found when adding jump-box");
289
+ const result = await getSecurityGroupId(r.value);
290
+ let sgId;
291
+ if (result.isErr())
292
+ return err(result.error);
293
+ else
294
+ sgId = result.value;
295
+ if (!sgId)
296
+ return err("Security group not found when adding jump-box");
297
+ const client = new EFSClient({ region: "us-east-1" });
298
+ const command = new DescribeFileSystemsCommand({});
299
+ const data = await client.send(command);
300
+ const fileSystemName = `stacks-${config2.app.env}-efs`;
301
+ const fileSystem = data.FileSystems?.find((fs) => fs.Name === fileSystemName);
302
+ const fileSystemId = fileSystem?.FileSystemId;
303
+ if (!fileSystem || !fileSystemId)
304
+ return err(`EFS file system ${fileSystemName} not found`);
305
+ const userDataScript = `
306
+ #!/bin/bash
307
+ yum update -y
308
+ yum install -y amazon-efs-utils
309
+ yum install -y git
310
+ yum install -y https://s3.us-east-1.amazonaws.com/amazon-ssm-us-east-1/latest/linux_amd64/amazon-ssm-agent.rpm
311
+ mkdir /mnt/efs
312
+ mount -t efs ${fileSystemId}:/ /mnt/efs
313
+ git clone https://github.com/stacksjs/stacks.git /mnt/efs
314
+ `;
315
+ const base64UserData = btoa(userDataScript);
316
+ const res = await getJumpBoxInstanceProfileName();
317
+ if (res.isErr())
318
+ return err(res.error);
319
+ const jumpBoxInstanceProfileName = res.value;
320
+ if (!jumpBoxInstanceProfileName)
321
+ return err("Jump-box IAM instance profile not found");
322
+ const instance = await ec2.runInstances({
323
+ ImageId: "ami-03a6eaae9938c858c",
324
+ InstanceType: InstanceType.t2_micro,
325
+ MaxCount: 1,
326
+ MinCount: 1,
327
+ SecurityGroupIds: [sgId],
328
+ SubnetId: "subnet-004c5f196358b00f0",
329
+ TagSpecifications: [
330
+ {
331
+ ResourceType: "instance",
332
+ Tags: [
333
+ {
334
+ Key: "Name",
335
+ Value: `${cloudName}-jump-box`
336
+ }
337
+ ]
338
+ }
339
+ ],
340
+ UserData: base64UserData,
341
+ IamInstanceProfile: {
342
+ Name: jumpBoxInstanceProfileName
343
+ }
344
+ });
345
+ return instance.Instances && instance.Instances[0] ? ok(`Jump-box created with id ${instance.Instances[0].InstanceId}`) : err("Jump-box creation failed");
346
+ }
347
+ async function getJumpBoxSecurityGroupName() {
348
+ const jumpBoxId = await getJumpBoxInstanceId();
349
+ if (!jumpBoxId)
350
+ return err("Jump-box not found");
351
+ const ec2 = new EC2({ region: "us-east-1" });
352
+ const data = await ec2.describeInstances({ InstanceIds: [jumpBoxId] });
353
+ if (data.Reservations && data.Reservations[0] && data.Reservations[0].Instances && data.Reservations[0].Instances[0]) {
354
+ const instance = data.Reservations[0].Instances[0];
355
+ const securityGroups = instance.SecurityGroups;
356
+ if (securityGroups && securityGroups[0])
357
+ return ok(securityGroups[0].GroupName);
358
+ }
359
+ return err("Security group not found");
360
+ }
361
+ async function getSecurityGroupFromInstanceId(instanceId) {
362
+ const ec2 = new EC2({ region: "us-east-1" });
363
+ const data = await ec2.describeInstances({ InstanceIds: [instanceId] });
364
+ if (data.Reservations && data.Reservations[0] && data.Reservations[0].Instances && data.Reservations[0].Instances[0]) {
365
+ const instance = data.Reservations[0].Instances[0];
366
+ const securityGroups = instance.SecurityGroups;
367
+ if (securityGroups && securityGroups[0])
368
+ return securityGroups[0].GroupId;
369
+ }
370
+ return;
371
+ }
372
+ async function isFirstDeployment() {
373
+ const stackName = cloudName;
374
+ const cloudFormation = new CloudFormation;
375
+ const data = await cloudFormation.listStacks({ StackStatusFilter: ["CREATE_COMPLETE", "UPDATE_COMPLETE"] });
376
+ const isStacksCloudPresent = data.StackSummaries?.some((stack) => stack.StackName === stackName);
377
+ return !isStacksCloudPresent;
378
+ }
379
+ async function isFailedState() {
380
+ const cloudFormation = new CloudFormation;
381
+ const data = await cloudFormation.listStacks({ StackStatusFilter: ["CREATE_FAILED", "UPDATE_FAILED", "ROLLBACK_COMPLETE", "UPDATE_ROLLBACK_COMPLETE"] });
382
+ const isStacksCloudPresent = data.StackSummaries?.some((stack) => stack.StackName === cloudName);
383
+ return !isStacksCloudPresent;
384
+ }
385
+ async function getOrCreateTimestamp() {
386
+ const parameterName = `/stacks/timestamp`;
387
+ const ssm = new SSM({ region: "us-east-1" });
388
+ try {
389
+ const response = await ssm.getParameter({ Name: parameterName });
390
+ const timestamp = response.Parameter ? response.Parameter.Value : undefined;
391
+ if (!timestamp)
392
+ throw new Error("Timestamp parameter not found");
393
+ return timestamp;
394
+ } catch (error) {
395
+ const timestamp = new Date().getTime().toString();
396
+ log.debug(`Creating timestamp parameter ${parameterName} with value ${timestamp}`);
397
+ await ssm.putParameter({
398
+ Name: parameterName,
399
+ Value: timestamp,
400
+ Type: "String"
401
+ });
402
+ return timestamp;
403
+ }
404
+ }
405
+ var appEnv = config2.app.env === "local" ? "dev" : config2.app.env;
406
+ var cloudName = `stacks-cloud-${appEnv}`;
407
+ // src/cloud/index.ts
408
+ import {Stack as Stack2} from "aws-cdk-lib";
409
+
410
+ // src/cloud/ai.ts
411
+ import {Duration, CfnOutput as Output, aws_iam as iam, aws_lambda as lambda} from "aws-cdk-lib";
412
+ import {config as config4} from "@stacksjs/config";
413
+
414
+ class AiStack {
415
+ askAiUrl;
416
+ summarizeAiUrl;
417
+ constructor(scope, props) {
418
+ const awsSdkLayer = new lambda.LayerVersion(scope, "AwsSdkLayer", {
419
+ code: lambda.Code.fromAsset("src/cloud/aws-sdk-layer"),
420
+ compatibleRuntimes: [lambda.Runtime.NODEJS_20_X],
421
+ description: "Layer with aws-sdk"
422
+ });
423
+ const bedrockAccessPolicy = new iam.PolicyStatement({
424
+ effect: iam.Effect.ALLOW,
425
+ actions: [
426
+ "bedrock:InvokeModel",
427
+ "bedrock:InvokeModelWithResponseStream"
428
+ ],
429
+ resources: config4.ai.models?.map((model) => `arn:aws:bedrock:us-east-1::foundation-model/${model}`)
430
+ });
431
+ const bedrockAccessRole = new iam.Role(scope, "BedrockAccessRole", {
432
+ assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
433
+ managedPolicies: [
434
+ iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
435
+ ]
436
+ });
437
+ bedrockAccessRole.addToPolicy(bedrockAccessPolicy);
438
+ const askAi = new lambda.Function(scope, "AskAiFunction", {
439
+ functionName: `${props.slug}-${props.appEnv}-ai-ask`,
440
+ description: "Lambda function to invoke the AI model",
441
+ runtime: lambda.Runtime.NODEJS_20_X,
442
+ handler: "index.handler",
443
+ code: lambda.Code.fromAsset("src/cloud/lambda/ask"),
444
+ layers: [awsSdkLayer],
445
+ role: bedrockAccessRole,
446
+ timeout: Duration.seconds(30)
447
+ });
448
+ this.askAiUrl = new lambda.FunctionUrl(scope, "AskAiFunctionUrl", {
449
+ function: askAi,
450
+ authType: lambda.FunctionUrlAuthType.NONE,
451
+ cors: {
452
+ allowedOrigins: ["*"]
453
+ }
454
+ });
455
+ const summarizeAi = new lambda.Function(scope, "SummarizeAiFunction", {
456
+ functionName: `${props.slug}-${props.appEnv}-ai-summarize`,
457
+ description: "Lambda function to summarize any given text",
458
+ runtime: lambda.Runtime.NODEJS_20_X,
459
+ handler: "index.handler",
460
+ code: lambda.Code.fromAsset("src/cloud/lambda/summarize"),
461
+ layers: [awsSdkLayer],
462
+ role: bedrockAccessRole,
463
+ timeout: Duration.seconds(30)
464
+ });
465
+ this.summarizeAiUrl = new lambda.FunctionUrl(scope, "SummarizeAiFunctionUrl", {
466
+ function: summarizeAi,
467
+ authType: lambda.FunctionUrlAuthType.NONE,
468
+ cors: {
469
+ allowedOrigins: ["*"]
470
+ }
471
+ });
472
+ new Output(scope, "AiVanityAskApiUrl", {
473
+ value: this.askAiUrl.url
474
+ });
475
+ new Output(scope, "AiVanitySummarizeApiUrl", {
476
+ value: this.summarizeAiUrl.url
477
+ });
478
+ new Output(scope, "AiAskApiUrl", {
479
+ value: `https://${props.domain}/ai/ask`
480
+ });
481
+ new Output(scope, "AiSummarizeApiUrl", {
482
+ value: `https://${props.domain}/ai/summary`
483
+ });
484
+ }
485
+ }
486
+
487
+ // src/cloud/cdn.ts
488
+ 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";
489
+ import {config as config6} from "@stacksjs/config";
490
+
491
+ // /home/runner/work/stacks/stacks/storage/framework/core/storage/src/files.ts
492
+ import {detectIndent, detectNewline} from "@stacksjs/strings";
493
+ import {dirname, join as join2, path as p2} from "@stacksjs/path";
494
+
495
+ // /home/runner/work/stacks/stacks/storage/framework/core/arrays/src/helpers.ts
496
+ import {clamp} from "@stacksjs/utils";
497
+ // /home/runner/work/stacks/stacks/storage/framework/core/arrays/src/contains.ts
498
+ function contains(needle, haystack) {
499
+ return haystack.some((hay) => needle.includes(hay));
500
+ }
501
+ // /home/runner/work/stacks/stacks/storage/framework/core/storage/src/fs.ts
502
+ import {mkdirSync, writeFileSync} from "fs";
503
+ import * as fs from "fs-extra";
504
+ import {pathExists as existsSync} from "fs-extra";
505
+ async function exists(path2) {
506
+ return await existsSync(path2);
507
+ }
508
+
509
+ // /home/runner/work/stacks/stacks/storage/framework/core/storage/src/files.ts
510
+ async function readJsonFile(name, cwd) {
511
+ const file = await readTextFile(name, cwd);
512
+ const data = JSON.parse(file.data);
513
+ const indent = detectIndent(file.data).indent;
514
+ const newline = detectNewline(file.data);
515
+ return { ...file, data, indent, newline };
516
+ }
517
+ async function readPackageJson(name, cwd) {
518
+ const file = await readJsonFile(name, cwd);
519
+ return file.data;
520
+ }
521
+ async function writeFile(path3, data) {
522
+ if (typeof path3 === "string") {
523
+ const dirPath = dirname(path3);
524
+ if (!await existsSync(dirPath))
525
+ await createFolder(dirPath);
526
+ return await Bun.write(Bun.file(path3), data);
527
+ }
528
+ return await Bun.write(path3, data);
529
+ }
530
+ async function writeJsonFile(file) {
531
+ let json = JSON.stringify(file.data, undefined, file.indent);
532
+ if (file.newline)
533
+ json += file.newline;
534
+ return writeTextFile({ ...file, data: json });
535
+ }
536
+ function readTextFile(name, cwd) {
537
+ return new Promise((resolve, reject) => {
538
+ let filePath;
539
+ if (cwd)
540
+ filePath = join2(cwd, name);
541
+ else
542
+ filePath = name;
543
+ fs.readFile(filePath, "utf8", (err2, text) => {
544
+ if (err2) {
545
+ reject(err2);
546
+ } else {
547
+ resolve({
548
+ path: filePath,
549
+ data: text
550
+ });
551
+ }
552
+ });
553
+ });
554
+ }
555
+ async function writeTextFile(file) {
556
+ return await Bun.write(file.path, file.data);
557
+ }
558
+ function isFile(path3) {
559
+ return fs.existsSync(path3);
560
+ }
561
+ function doesExist(path3) {
562
+ return !isFile(path3) || !isFolder(path3);
563
+ }
564
+ function doesNotExist(path3) {
565
+ return !isFile(path3) && !isFolder(path3);
566
+ }
567
+ function hasFiles(folder) {
568
+ try {
569
+ return fs.readdirSync(folder).length > 0;
570
+ } catch (err2) {
571
+ return false;
572
+ }
573
+ }
574
+ function hasComponents() {
575
+ return hasFiles(p2.componentsPath());
576
+ }
577
+ function hasFunctions() {
578
+ return hasFiles(p2.functionsPath());
579
+ }
580
+ function deleteFiles(dir, exclude = []) {
581
+ if (fs.existsSync(dir)) {
582
+ fs.readdirSync(dir).forEach((file) => {
583
+ const p3 = join2(dir, file);
584
+ if (fs.statSync(p3).isDirectory()) {
585
+ if (fs.readdirSync(p3).length === 0)
586
+ fs.rmSync(p3, { recursive: true, force: true });
587
+ else
588
+ deleteFiles(p3, exclude);
589
+ } else if (!contains(p3, exclude)) {
590
+ fs.rmSync(p3);
591
+ }
592
+ });
593
+ }
594
+ }
595
+ function getFiles(dir, exclude = []) {
596
+ let results = [];
597
+ const list = fs.readdirSync(dir);
598
+ list.forEach((file) => {
599
+ file = join2(dir, file);
600
+ const stat = fs.statSync(file);
601
+ if (stat && stat.isDirectory())
602
+ results = results.concat(getFiles(file, exclude));
603
+ else if (!contains(file, exclude))
604
+ results.push(file);
605
+ });
606
+ return results;
607
+ }
608
+ function put(path3, contents) {
609
+ const dirPath = dirname(path3);
610
+ if (!fs.existsSync(dirPath))
611
+ fs.mkdirSync(dirPath, { recursive: true });
612
+ fs.writeFileSync(path3, contents, "utf-8");
613
+ }
614
+ async function get(path3) {
615
+ return Bun.file(path3).text();
616
+ }
617
+ var files = {
618
+ readJsonFile,
619
+ readPackageJson,
620
+ readTextFile,
621
+ writeJsonFile,
622
+ writeTextFile,
623
+ isFile,
624
+ hasFiles,
625
+ hasComponents,
626
+ hasFunctions,
627
+ deleteFiles,
628
+ getFiles,
629
+ put,
630
+ get
631
+ };
632
+ // /home/runner/work/stacks/stacks/storage/framework/core/storage/src/folders.ts
633
+ import {join as join3} from "@stacksjs/path";
634
+ function isFolder2(path4) {
635
+ try {
636
+ return fs.statSync(path4).isDirectory();
637
+ } catch {
638
+ return false;
639
+ }
640
+ }
641
+ function isDirectory(path4) {
642
+ return isFolder2(path4);
643
+ }
644
+ function isDir(path4) {
645
+ return isFolder2(path4);
646
+ }
647
+ function doesFolderExist(path4) {
648
+ return fs.existsSync(path4);
649
+ }
650
+ function createFolder2(dir) {
651
+ return new Promise((resolve, reject) => {
652
+ fs.mkdirs(dir, (err2) => {
653
+ if (err2)
654
+ reject(err2);
655
+ else
656
+ resolve();
657
+ });
658
+ });
659
+ }
660
+ function getFolders(dir) {
661
+ return fs.readdirSync(dir).filter((file) => {
662
+ return fs.statSync(join3(dir, file)).isDirectory();
663
+ });
664
+ }
665
+ var folders = {
666
+ isFolder: isFolder2,
667
+ doesFolderExist,
668
+ createFolder: createFolder2,
669
+ getFolders
670
+ };
671
+ // /home/runner/work/stacks/stacks/storage/framework/core/storage/src/hash.ts
672
+ import {path as p3} from "@stacksjs/path";
673
+ // /home/runner/work/stacks/stacks/storage/framework/core/storage/src/helpers.ts
674
+ import {fileURLToPath} from "url";
675
+ import {dirname as dirname2} from "@stacksjs/path";
676
+ function updateConfigFile(filePath, newConfig) {
677
+ return new Promise((resolve, reject) => {
678
+ const config5 = JSON.parse(fs.readFileSync(filePath, "utf8"));
679
+ for (const key in newConfig)
680
+ config5[key] = newConfig[key];
681
+ try {
682
+ fs.writeFileSync(filePath, JSON.stringify(config5, null, 2));
683
+ resolve();
684
+ } catch (error) {
685
+ reject(error);
686
+ }
687
+ });
688
+ }
689
+ var __dirname = "/home/runner/work/stacks/stacks/storage/framework/core/storage/src";
690
+ var _dirname = typeof __dirname !== "undefined" ? __dirname : dirname2(fileURLToPath(import.meta.url));
691
+ var helpers3 = {
692
+ _dirname,
693
+ updateConfigFile
694
+ };
695
+ // /home/runner/work/stacks/stacks/storage/framework/core/storage/src/copy.ts
696
+ import {join as join4} from "@stacksjs/path";
697
+ function copy(src, dest, exclude = []) {
698
+ if (Array.isArray(src)) {
699
+ src.forEach((file) => {
700
+ copy(file, dest, exclude);
701
+ });
702
+ } else {
703
+ if (fs.statSync(src).isDirectory())
704
+ copyFolder(src, dest, exclude);
705
+ else
706
+ copyFile(src, dest);
707
+ }
708
+ }
709
+ function copyFile(src, dest) {
710
+ fs.copyFileSync(src, dest);
711
+ }
712
+ function copyFolder(src, dest, exclude = []) {
713
+ if (!fs.existsSync(dest))
714
+ fs.mkdirSync(dest, { recursive: true });
715
+ if (fs.existsSync(src)) {
716
+ fs.readdirSync(src).forEach((file) => {
717
+ if (!contains(join4(src, file), exclude)) {
718
+ const srcPath = join4(src, file);
719
+ const destPath = join4(dest, file);
720
+ if (fs.statSync(srcPath).isDirectory())
721
+ copyFolder(srcPath, destPath, exclude);
722
+ else
723
+ fs.copyFileSync(srcPath, destPath);
724
+ }
725
+ });
726
+ }
727
+ }
728
+ // /home/runner/work/stacks/stacks/storage/framework/core/storage/src/glob.ts
729
+ import {default as default2} from "fast-glob";
730
+ // /home/runner/work/stacks/stacks/storage/framework/core/storage/src/delete.ts
731
+ import {err as err2, ok as ok2} from "@stacksjs/error-handling";
732
+ import {italic, log as log2} from "@stacksjs/cli";
733
+ function deleteFolder(path7) {
734
+ return new Promise((resolve, reject) => {
735
+ try {
736
+ if (isFolder2(path7)) {
737
+ fs.rmSync(path7, { recursive: true, force: true });
738
+ return resolve(ok2(`Deleted ${path7}`));
739
+ }
740
+ return resolve(ok2(`Path ${path7} was not a directory`));
741
+ } catch (error) {
742
+ return reject(err2(error));
743
+ }
744
+ });
745
+ }
746
+ async function isDirectoryEmpty(path7) {
747
+ return new Promise((resolve, reject) => {
748
+ try {
749
+ if (fs.statSync(path7).isDirectory()) {
750
+ if (fs.readdirSync(path7).length === 0)
751
+ return resolve(ok2(true));
752
+ else
753
+ return resolve(ok2(false));
754
+ }
755
+ return resolve(ok2(false));
756
+ } catch (error) {
757
+ return reject(err2(error));
758
+ }
759
+ });
760
+ }
761
+ async function deleteEmptyFolder(path7) {
762
+ return new Promise((resolve, reject) => {
763
+ try {
764
+ if (fs.statSync(path7).isDirectory()) {
765
+ if (fs.readdirSync(path7).length === 0) {
766
+ fs.rmSync(path7, { recursive: true, force: true });
767
+ return resolve(ok2(`Deleted ${path7}`));
768
+ } else {
769
+ return resolve(ok2(`Path ${path7} was not empty`));
770
+ }
771
+ }
772
+ return resolve(ok2(`Path ${path7} was not a directory`));
773
+ } catch (error) {
774
+ return reject(err2(error));
775
+ }
776
+ });
777
+ }
778
+ async function deleteEmptyFolders(dir) {
779
+ try {
780
+ if (!fs.existsSync(dir))
781
+ return ok2(`Path ${dir} does not exist`);
782
+ const files3 = fs.readdirSync(dir);
783
+ for (const file of files3) {
784
+ const p4 = join(dir, file);
785
+ if (isFolder2(p4)) {
786
+ if (fs.readdirSync(p4).length === 0)
787
+ fs.rmSync(p4, { recursive: true, force: true });
788
+ else
789
+ await deleteEmptyFolders(p4);
790
+ }
791
+ }
792
+ return ok2(`Deleted empty folders located in ${dir}`);
793
+ } catch (error) {
794
+ return err2(error);
795
+ }
796
+ }
797
+ function deleteFile(path7) {
798
+ return new Promise((resolve, reject) => {
799
+ try {
800
+ if (isFile(path7)) {
801
+ fs.rmSync(path7, { recursive: true, force: true });
802
+ return resolve(ok2(`Deleted ${path7}`));
803
+ }
804
+ return resolve(ok2(`Path ${path7} was not a file`));
805
+ } catch (error) {
806
+ return reject(err2(error));
807
+ }
808
+ });
809
+ }
810
+ async function deleteGlob(path7) {
811
+ if (!path7.includes("*"))
812
+ return err2(handleError(`Path ${path7} does not contain a glob`));
813
+ const directories = await default2([path7], { onlyDirectories: true });
814
+ for (const directory of directories) {
815
+ const result = await deleteFolder(directory);
816
+ if (result.isErr()) {
817
+ log2.error(result.error);
818
+ return result;
819
+ }
820
+ log2.info(`Deleted ${italic(directory)}`);
821
+ }
822
+ return ok2(`Deleted ${directories.length} directories`);
823
+ }
824
+ async function del(path7) {
825
+ if (isFile(path7))
826
+ return await deleteFile(path7);
827
+ if (isFolder2(path7))
828
+ return await deleteFolder(path7);
829
+ if (path7.includes("*"))
830
+ return await deleteGlob(path7);
831
+ return err2(handleError(`Path ${path7} cannot be deleted due to an unhandled condition. Please report this issue.`));
832
+ }
833
+ // /home/runner/work/stacks/stacks/storage/framework/core/storage/src/zip.ts
834
+ import {runCommand} from "@stacksjs/cli";
835
+ async function zip(from, to, options) {
836
+ const toPath = to || "archive.zip";
837
+ const fromPath = Array.isArray(from) ? from.join(" ") : from;
838
+ if (Array.isArray(from))
839
+ return runCommand(`zip -r ${toPath} ${fromPath}`, options);
840
+ return runCommand(`zip -r ${to} ${from}`, options);
841
+ }
842
+ async function unzip(paths) {
843
+ if (Array.isArray(paths))
844
+ return runCommand(`unzip ${paths.join(" ")}`);
845
+ return runCommand(`unzip ${paths}`);
846
+ }
847
+ function archive(paths) {
848
+ return zip(paths);
849
+ }
850
+ function unarchive(paths) {
851
+ return unzip(paths);
852
+ }
853
+ function compress(paths) {
854
+ return zip(paths);
855
+ }
856
+ function decompress(paths) {
857
+ return unzip(paths);
858
+ }
859
+ function gzipSync(data, options) {
860
+ return Bun.gzipSync(data, options);
861
+ }
862
+ function gunzipSync(data) {
863
+ return Bun.gunzipSync(data);
864
+ }
865
+ function deflateSync(data, options) {
866
+ return Bun.deflateSync(data, options);
867
+ }
868
+ function inflateSync(data) {
869
+ return Bun.inflateSync(data);
870
+ }
871
+ // /home/runner/work/stacks/stacks/storage/framework/core/storage/src/storage.ts
872
+ var exports_storage = {};
873
+ __export(exports_storage, {
874
+ zip: () => {
875
+ {
876
+ return zip;
877
+ }
878
+ },
879
+ writeTextFile: () => {
880
+ {
881
+ return writeTextFile;
882
+ }
883
+ },
884
+ writeJsonFile: () => {
885
+ {
886
+ return writeJsonFile;
887
+ }
888
+ },
889
+ writeFileSync: () => {
890
+ {
891
+ return writeFileSync;
892
+ }
893
+ },
894
+ writeFile: () => {
895
+ {
896
+ return writeFile;
897
+ }
898
+ },
899
+ updateConfigFile: () => {
900
+ {
901
+ return updateConfigFile;
902
+ }
903
+ },
904
+ unzip: () => {
905
+ {
906
+ return unzip;
907
+ }
908
+ },
909
+ unarchive: () => {
910
+ {
911
+ return unarchive;
912
+ }
913
+ },
914
+ setVisibility: () => {
915
+ {
916
+ return setVisibility;
917
+ }
918
+ },
919
+ rename: () => {
920
+ {
921
+ return rename;
922
+ }
923
+ },
924
+ readTextFile: () => {
925
+ {
926
+ return readTextFile;
927
+ }
928
+ },
929
+ readPackageJson: () => {
930
+ {
931
+ return readPackageJson;
932
+ }
933
+ },
934
+ readJsonFile: () => {
935
+ {
936
+ return readJsonFile;
937
+ }
938
+ },
939
+ put: () => {
940
+ {
941
+ return put;
942
+ }
943
+ },
944
+ move: () => {
945
+ {
946
+ return move2;
947
+ }
948
+ },
949
+ mkdirSync: () => {
950
+ {
951
+ return mkdirSync;
952
+ }
953
+ },
954
+ isFolder: () => {
955
+ {
956
+ return isFolder2;
957
+ }
958
+ },
959
+ isFile: () => {
960
+ {
961
+ return isFile;
962
+ }
963
+ },
964
+ isDirectoryEmpty: () => {
965
+ {
966
+ return isDirectoryEmpty;
967
+ }
968
+ },
969
+ isDirectory: () => {
970
+ {
971
+ return isDirectory;
972
+ }
973
+ },
974
+ isDir: () => {
975
+ {
976
+ return isDir;
977
+ }
978
+ },
979
+ inflateSync: () => {
980
+ {
981
+ return inflateSync;
982
+ }
983
+ },
984
+ helpers: () => {
985
+ {
986
+ return helpers3;
987
+ }
988
+ },
989
+ hasFunctions: () => {
990
+ {
991
+ return hasFunctions;
992
+ }
993
+ },
994
+ hasFiles: () => {
995
+ {
996
+ return hasFiles;
997
+ }
998
+ },
999
+ hasComponents: () => {
1000
+ {
1001
+ return hasComponents;
1002
+ }
1003
+ },
1004
+ gzipSync: () => {
1005
+ {
1006
+ return gzipSync;
1007
+ }
1008
+ },
1009
+ gunzipSync: () => {
1010
+ {
1011
+ return gunzipSync;
1012
+ }
1013
+ },
1014
+ getFolders: () => {
1015
+ {
1016
+ return getFolders;
1017
+ }
1018
+ },
1019
+ getFiles: () => {
1020
+ {
1021
+ return getFiles;
1022
+ }
1023
+ },
1024
+ get: () => {
1025
+ {
1026
+ return get;
1027
+ }
1028
+ },
1029
+ fs: () => {
1030
+ {
1031
+ return fs;
1032
+ }
1033
+ },
1034
+ folders: () => {
1035
+ {
1036
+ return folders;
1037
+ }
1038
+ },
1039
+ files: () => {
1040
+ {
1041
+ return files;
1042
+ }
1043
+ },
1044
+ existsSync: () => {
1045
+ {
1046
+ return existsSync;
1047
+ }
1048
+ },
1049
+ exists: () => {
1050
+ {
1051
+ return exists;
1052
+ }
1053
+ },
1054
+ doesNotExist: () => {
1055
+ {
1056
+ return doesNotExist;
1057
+ }
1058
+ },
1059
+ doesFolderExist: () => {
1060
+ {
1061
+ return doesFolderExist;
1062
+ }
1063
+ },
1064
+ doesExist: () => {
1065
+ {
1066
+ return doesExist;
1067
+ }
1068
+ },
1069
+ deleteGlob: () => {
1070
+ {
1071
+ return deleteGlob;
1072
+ }
1073
+ },
1074
+ deleteFolder: () => {
1075
+ {
1076
+ return deleteFolder;
1077
+ }
1078
+ },
1079
+ deleteFiles: () => {
1080
+ {
1081
+ return deleteFiles;
1082
+ }
1083
+ },
1084
+ deleteFile: () => {
1085
+ {
1086
+ return deleteFile;
1087
+ }
1088
+ },
1089
+ deleteEmptyFolders: () => {
1090
+ {
1091
+ return deleteEmptyFolders;
1092
+ }
1093
+ },
1094
+ deleteEmptyFolder: () => {
1095
+ {
1096
+ return deleteEmptyFolder;
1097
+ }
1098
+ },
1099
+ del: () => {
1100
+ {
1101
+ return del;
1102
+ }
1103
+ },
1104
+ deflateSync: () => {
1105
+ {
1106
+ return deflateSync;
1107
+ }
1108
+ },
1109
+ decompress: () => {
1110
+ {
1111
+ return decompress;
1112
+ }
1113
+ },
1114
+ createFolder: () => {
1115
+ {
1116
+ return createFolder2;
1117
+ }
1118
+ },
1119
+ copyFolder: () => {
1120
+ {
1121
+ return copyFolder;
1122
+ }
1123
+ },
1124
+ copyFile: () => {
1125
+ {
1126
+ return copyFile;
1127
+ }
1128
+ },
1129
+ copy: () => {
1130
+ {
1131
+ return copy;
1132
+ }
1133
+ },
1134
+ compress: () => {
1135
+ {
1136
+ return compress;
1137
+ }
1138
+ },
1139
+ archive: () => {
1140
+ {
1141
+ return archive;
1142
+ }
1143
+ },
1144
+ _dirname: () => {
1145
+ {
1146
+ return _dirname;
1147
+ }
1148
+ }
1149
+ });
1150
+
1151
+ // /home/runner/work/stacks/stacks/storage/framework/core/storage/src/move.ts
1152
+ import {err as err3, ok as ok3} from "@stacksjs/error-handling";
1153
+ import {log as log3} from "@stacksjs/logging";
1154
+ import {path as path8} from "@stacksjs/path";
1155
+ async function move2(src, dest, options) {
1156
+ try {
1157
+ if (Array.isArray(src)) {
1158
+ const operations = src.map(async (file) => {
1159
+ const from2 = file;
1160
+ const to2 = path8.resolve(dest, path8.basename(file));
1161
+ const result2 = await rename(from2, to2, options);
1162
+ if (result2.isErr()) {
1163
+ log3.error(result2.error);
1164
+ return err3(handleError(result2.error.message, result2.error));
1165
+ }
1166
+ });
1167
+ await Promise.all(operations);
1168
+ return ok3({ message: "Files moved successfully" });
1169
+ }
1170
+ const from = src;
1171
+ const to = dest;
1172
+ const result = await rename(from, to, options);
1173
+ if (result.isErr()) {
1174
+ log3.error(result.error);
1175
+ return err3(handleError(result.error));
1176
+ }
1177
+ return ok3({ message: "File moved successfully" });
1178
+ } catch (error) {
1179
+ return err3(handleError(error));
1180
+ }
1181
+ }
1182
+ async function rename(from, to, options) {
1183
+ return new Promise((resolve, reject) => {
1184
+ try {
1185
+ const dir = path8.dirname(to);
1186
+ if (!fs.existsSync(dir))
1187
+ fs.mkdirSync(dir, { recursive: true });
1188
+ if (!fs.existsSync(from))
1189
+ return reject(err3(new Error(`File or directory does not exist: ${from}`)));
1190
+ if (fs.existsSync(to)) {
1191
+ if (!options?.overwrite)
1192
+ return reject(err3(new Error(`File or directory already exists: ${to}`)));
1193
+ fs.unlinkSync(to);
1194
+ }
1195
+ fs.renameSync(from, to);
1196
+ return resolve(ok3({ message: "File moved successfully" }));
1197
+ } catch (error) {
1198
+ if (error.code === "ENOENT")
1199
+ log3.error("File or directory does not exist\n\n", error);
1200
+ else
1201
+ log3.error(error);
1202
+ return reject(err3(new Error(error)));
1203
+ }
1204
+ });
1205
+ }
1206
+ // /home/runner/work/stacks/stacks/storage/framework/core/storage/src/visibility.ts
1207
+ function setVisibility() {
1208
+ return "wip";
1209
+ }
1210
+ // src/cloud/cdn.ts
1211
+ import {path as p4} from "@stacksjs/path";
1212
+ import {env as env2} from "@stacksjs/env";
1213
+
1214
+ class CdnStack {
1215
+ distribution;
1216
+ originAccessIdentity;
1217
+ cdnCachePolicy;
1218
+ apiCachePolicy;
1219
+ vanityUrl;
1220
+ props;
1221
+ constructor(scope, props) {
1222
+ this.props = props;
1223
+ this.originAccessIdentity = new cloudfront.OriginAccessIdentity(scope, "OAI");
1224
+ this.cdnCachePolicy = new cloudfront.CachePolicy(scope, "CdnCachePolicy", {
1225
+ comment: "Stacks CDN Cache Policy",
1226
+ cachePolicyName: `${props.slug}-${props.appEnv}-cdn-cache-policy`,
1227
+ minTtl: config6.cloud.cdn?.minTtl ? Duration2.seconds(config6.cloud.cdn.minTtl) : undefined,
1228
+ defaultTtl: config6.cloud.cdn?.defaultTtl ? Duration2.seconds(config6.cloud.cdn.defaultTtl) : undefined,
1229
+ maxTtl: config6.cloud.cdn?.maxTtl ? Duration2.seconds(config6.cloud.cdn.maxTtl) : undefined,
1230
+ cookieBehavior: this.getCookieBehavior(config6.cloud.cdn?.cookieBehavior)
1231
+ });
1232
+ this.distribution = new cloudfront.Distribution(scope, "Cdn", {
1233
+ domainNames: [props.domain],
1234
+ defaultRootObject: "index.html",
1235
+ comment: `CDN for ${config6.app.url}`,
1236
+ certificate: props.certificate,
1237
+ enableLogging: true,
1238
+ logBucket: props.logBucket,
1239
+ httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
1240
+ priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
1241
+ enabled: true,
1242
+ minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
1243
+ webAclId: props.firewall.attrArn,
1244
+ enableIpv6: true,
1245
+ defaultBehavior: {
1246
+ origin: new origins.S3Origin(props.publicBucket, {
1247
+ originAccessIdentity: this.originAccessIdentity
1248
+ }),
1249
+ edgeLambdas: [
1250
+ {
1251
+ eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
1252
+ functionVersion: props.originRequestFunction.currentVersion
1253
+ }
1254
+ ],
1255
+ compress: config6.cloud.cdn?.compress,
1256
+ allowedMethods: this.allowedMethods(),
1257
+ cachedMethods: this.cachedMethods(),
1258
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
1259
+ cachePolicy: this.cdnCachePolicy
1260
+ },
1261
+ additionalBehaviors: this.additionalBehaviors(scope, props),
1262
+ errorResponses: [
1263
+ {
1264
+ httpStatus: 403,
1265
+ responsePagePath: "/index.html",
1266
+ responseHttpStatus: 200,
1267
+ ttl: Duration2.seconds(0)
1268
+ }
1269
+ ]
1270
+ });
1271
+ new route53.ARecord(scope, "AliasRecord", {
1272
+ recordName: props.domain,
1273
+ zone: props.zone,
1274
+ target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(this.distribution))
1275
+ });
1276
+ new Output2(scope, "DistributionId", {
1277
+ value: this.distribution.distributionId
1278
+ });
1279
+ new Output2(scope, "AppUrl", {
1280
+ value: `https://${props.domain}`,
1281
+ description: "The URL of the deployed application"
1282
+ });
1283
+ this.vanityUrl = `https://${this.distribution.domainName}`;
1284
+ new Output2(scope, "AppVanityUrl", {
1285
+ value: this.vanityUrl,
1286
+ description: "The vanity URL of the deployed application"
1287
+ });
1288
+ }
1289
+ getCookieBehavior(behavior) {
1290
+ switch (behavior) {
1291
+ case "all":
1292
+ return cloudfront.CacheCookieBehavior.all();
1293
+ case "none":
1294
+ return cloudfront.CacheCookieBehavior.none();
1295
+ case "allowList":
1296
+ return cloudfront.CacheCookieBehavior.allowList(...config6.cloud.cdn?.allowList.cookies || []);
1297
+ default:
1298
+ return;
1299
+ }
1300
+ }
1301
+ allowedMethods() {
1302
+ switch (config6.cloud.cdn?.allowedMethods) {
1303
+ case "ALL":
1304
+ return cloudfront.AllowedMethods.ALLOW_ALL;
1305
+ case "GET_HEAD":
1306
+ return cloudfront.AllowedMethods.ALLOW_GET_HEAD;
1307
+ case "GET_HEAD_OPTIONS":
1308
+ return cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS;
1309
+ default:
1310
+ return cloudfront.AllowedMethods.ALLOW_ALL;
1311
+ }
1312
+ }
1313
+ cachedMethods() {
1314
+ switch (config6.cloud.cdn?.cachedMethods) {
1315
+ case "GET_HEAD":
1316
+ return cloudfront.CachedMethods.CACHE_GET_HEAD;
1317
+ case "GET_HEAD_OPTIONS":
1318
+ return cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS;
1319
+ default:
1320
+ return cloudfront.CachedMethods.CACHE_GET_HEAD;
1321
+ }
1322
+ }
1323
+ allowedMethodsFromString(methods) {
1324
+ if (!methods)
1325
+ return cloudfront.AllowedMethods.ALLOW_ALL;
1326
+ switch (methods) {
1327
+ case "ALL":
1328
+ return cloudfront.AllowedMethods.ALLOW_ALL;
1329
+ case "GET_HEAD":
1330
+ return cloudfront.AllowedMethods.ALLOW_GET_HEAD;
1331
+ case "GET_HEAD_OPTIONS":
1332
+ return cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS;
1333
+ default:
1334
+ return cloudfront.AllowedMethods.ALLOW_ALL;
1335
+ }
1336
+ }
1337
+ cachedMethodsFromString(methods) {
1338
+ if (!methods)
1339
+ return cloudfront.CachedMethods.CACHE_GET_HEAD;
1340
+ switch (methods) {
1341
+ case "GET_HEAD":
1342
+ return cloudfront.CachedMethods.CACHE_GET_HEAD;
1343
+ case "GET_HEAD_OPTIONS":
1344
+ return cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS;
1345
+ default:
1346
+ return cloudfront.CachedMethods.CACHE_GET_HEAD;
1347
+ }
1348
+ }
1349
+ shouldDeployApi() {
1350
+ return config6.cloud.api;
1351
+ }
1352
+ apiBehaviorOptions(scope, props) {
1353
+ const hostname = Fn.select(2, Fn.split("/", props.webServerUrl.url));
1354
+ const origin = (path10 = "/api") => {
1355
+ return new origins.HttpOrigin(hostname, {
1356
+ originPath: path10,
1357
+ protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY
1358
+ });
1359
+ };
1360
+ return {
1361
+ "/api": {
1362
+ origin: origin(),
1363
+ compress: true,
1364
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
1365
+ cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
1366
+ cachePolicy: this.setApiCachePolicy(scope),
1367
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS
1368
+ },
1369
+ "/api/*": {
1370
+ origin: origin("/api/*"),
1371
+ compress: true,
1372
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
1373
+ cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
1374
+ cachePolicy: this.apiCachePolicy,
1375
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS
1376
+ }
1377
+ };
1378
+ }
1379
+ docsBehaviorOptions(props) {
1380
+ return {
1381
+ "/docs": {
1382
+ origin: new origins.S3Origin(props.publicBucket, {
1383
+ originAccessIdentity: this.originAccessIdentity,
1384
+ originPath: "/docs"
1385
+ }),
1386
+ compress: true,
1387
+ allowedMethods: this.allowedMethodsFromString(config6.cloud.cdn?.allowedMethods),
1388
+ cachedMethods: this.cachedMethodsFromString(config6.cloud.cdn?.cachedMethods),
1389
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
1390
+ cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED
1391
+ },
1392
+ "/docs/*": {
1393
+ origin: new origins.S3Origin(props.publicBucket, {
1394
+ originAccessIdentity: this.originAccessIdentity,
1395
+ originPath: "/docs"
1396
+ }),
1397
+ compress: true,
1398
+ allowedMethods: this.allowedMethodsFromString(config6.cloud.cdn?.allowedMethods),
1399
+ cachedMethods: this.cachedMethodsFromString(config6.cloud.cdn?.cachedMethods),
1400
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
1401
+ cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED
1402
+ }
1403
+ };
1404
+ }
1405
+ aiBehaviorOptions(scope, props) {
1406
+ const hostname = Fn.select(2, Fn.split("/", props.askAiUrl.url));
1407
+ const summaryHostname = Fn.select(2, Fn.split("/", props.summarizeAiUrl.url));
1408
+ const aiCachePolicy = new cloudfront.CachePolicy(scope, "AiCachePolicy", {
1409
+ comment: "Stacks AI Cache Policy",
1410
+ cachePolicyName: `${this.props.slug}-${this.props.appEnv}-ai-cache-policy`,
1411
+ defaultTtl: Duration2.seconds(0),
1412
+ cookieBehavior: cloudfront.CacheCookieBehavior.none(),
1413
+ headerBehavior: cloudfront.CacheHeaderBehavior.allowList("Accept", "x-api-key", "Authorization", "Content-Type"),
1414
+ queryStringBehavior: cloudfront.CacheQueryStringBehavior.all()
1415
+ });
1416
+ return {
1417
+ "/ai/ask": {
1418
+ origin: new origins.HttpOrigin(hostname, {
1419
+ originPath: "/ai",
1420
+ protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY
1421
+ }),
1422
+ compress: false,
1423
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
1424
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
1425
+ cachePolicy: aiCachePolicy
1426
+ },
1427
+ "/ai/summary": {
1428
+ origin: new origins.HttpOrigin(summaryHostname, {
1429
+ originPath: "/ai",
1430
+ protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY
1431
+ }),
1432
+ compress: false,
1433
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
1434
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
1435
+ cachePolicy: aiCachePolicy
1436
+ }
1437
+ };
1438
+ }
1439
+ cliSetupBehaviorOptions(scope, props) {
1440
+ const hostname = Fn.select(2, Fn.split("/", props.cliSetupUrl.url));
1441
+ return {
1442
+ "/install": {
1443
+ origin: new origins.HttpOrigin(hostname, {
1444
+ originPath: "/cli-setup",
1445
+ protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY
1446
+ }),
1447
+ compress: false,
1448
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
1449
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
1450
+ cachePolicy: new cloudfront.CachePolicy(scope, "CliSetupCachePolicy", {
1451
+ comment: "Stacks CLI Setup Cache Policy",
1452
+ cachePolicyName: `${this.props.slug}-${this.props.appEnv}-cli-setup-cache-policy`,
1453
+ defaultTtl: Duration2.seconds(0),
1454
+ cookieBehavior: cloudfront.CacheCookieBehavior.none(),
1455
+ headerBehavior: cloudfront.CacheHeaderBehavior.none(),
1456
+ queryStringBehavior: cloudfront.CacheQueryStringBehavior.none()
1457
+ })
1458
+ }
1459
+ };
1460
+ }
1461
+ shouldDeployAiEndpoints() {
1462
+ return config6.cloud.ai;
1463
+ }
1464
+ shouldDeployCliSetup() {
1465
+ return config6.cloud.cli;
1466
+ }
1467
+ shouldDeployDocs() {
1468
+ return hasFiles(p4.projectPath("docs"));
1469
+ }
1470
+ additionalBehaviors(scope, props) {
1471
+ let behaviorOptions = {};
1472
+ if (this.shouldDeployApi()) {
1473
+ 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", "_"];
1474
+ keysToRemove.forEach((key) => delete env2[key]);
1475
+ behaviorOptions = this.apiBehaviorOptions(scope, props);
1476
+ }
1477
+ if (this.shouldDeployDocs() && !config6.app.docMode) {
1478
+ behaviorOptions = {
1479
+ ...this.docsBehaviorOptions(props),
1480
+ ...behaviorOptions
1481
+ };
1482
+ }
1483
+ if (this.shouldDeployAiEndpoints()) {
1484
+ behaviorOptions = {
1485
+ ...this.aiBehaviorOptions(scope, props),
1486
+ ...behaviorOptions
1487
+ };
1488
+ }
1489
+ if (this.shouldDeployCliSetup()) {
1490
+ behaviorOptions = {
1491
+ ...this.cliSetupBehaviorOptions(scope, props),
1492
+ ...behaviorOptions
1493
+ };
1494
+ }
1495
+ return behaviorOptions;
1496
+ }
1497
+ setApiCachePolicy(scope) {
1498
+ if (this.apiCachePolicy)
1499
+ return this.apiCachePolicy;
1500
+ this.apiCachePolicy = new cloudfront.CachePolicy(scope, "ApiCachePolicy", {
1501
+ comment: "Stacks API Cache Policy",
1502
+ cachePolicyName: `${this.props.slug}-${this.props.appEnv}-api-cache-policy`,
1503
+ defaultTtl: Duration2.seconds(0),
1504
+ cookieBehavior: cloudfront.CacheCookieBehavior.none(),
1505
+ headerBehavior: cloudfront.CacheHeaderBehavior.allowList("Accept", "x-api-key", "Authorization", "Content-Type"),
1506
+ queryStringBehavior: cloudfront.CacheQueryStringBehavior.none()
1507
+ });
1508
+ return this.apiCachePolicy;
1509
+ }
1510
+ }
1511
+
1512
+ // src/cloud/cli.ts
1513
+ import {Duration as Duration3, CfnOutput as Output3, aws_lambda as lambda2} from "aws-cdk-lib";
1514
+
1515
+ class CliStack {
1516
+ cliSetupUrl;
1517
+ constructor(scope, props) {
1518
+ const cliSetupFunc = new lambda2.Function(scope, "CliSetupFunction", {
1519
+ functionName: `${props.slug}-${props.appEnv}-cli-setup`,
1520
+ description: "Lambda function that triggers setup script for a Stacks project",
1521
+ runtime: lambda2.Runtime.NODEJS_20_X,
1522
+ handler: "index.handler",
1523
+ code: lambda2.Code.fromAsset("src/cloud/lambda/cli-setup"),
1524
+ timeout: Duration3.seconds(30)
1525
+ });
1526
+ this.cliSetupUrl = new lambda2.FunctionUrl(scope, "CliSetupFunctionUrl", {
1527
+ function: cliSetupFunc,
1528
+ authType: lambda2.FunctionUrlAuthType.NONE,
1529
+ cors: {
1530
+ allowedOrigins: ["*"]
1531
+ }
1532
+ });
1533
+ new Output3(scope, "CliSetupVanityUrl", {
1534
+ value: `${this.cliSetupUrl.url}cli-setup`
1535
+ });
1536
+ new Output3(scope, "CliSetupUrl", {
1537
+ value: `https://${props.domain}/install`,
1538
+ description: "URL to trigger the CLI setup function"
1539
+ });
1540
+ }
1541
+ }
1542
+
1543
+ // src/cloud/dns.ts
1544
+ import {RemovalPolicy, aws_route53 as route532, aws_s3 as s3, aws_route53_targets as targets2} from "aws-cdk-lib";
1545
+
1546
+ class DnsStack {
1547
+ zone;
1548
+ constructor(scope, props) {
1549
+ this.zone = route532.PublicHostedZone.fromLookup(scope, "AppUrlHostedZone", {
1550
+ domainName: props.domain
1551
+ });
1552
+ const wwwBucket = new s3.Bucket(scope, "WwwBucket", {
1553
+ bucketName: `www.${props.domain}`,
1554
+ websiteRedirect: {
1555
+ hostName: props.domain,
1556
+ protocol: s3.RedirectProtocol.HTTPS
1557
+ },
1558
+ removalPolicy: RemovalPolicy.DESTROY,
1559
+ autoDeleteObjects: true
1560
+ });
1561
+ new route532.ARecord(scope, "WwwAliasRecord", {
1562
+ recordName: `www.${props.domain}`,
1563
+ zone: this.zone,
1564
+ target: route532.RecordTarget.fromAlias(new targets2.BucketWebsiteTarget(wwwBucket))
1565
+ });
1566
+ }
1567
+ }
1568
+
1569
+ // src/cloud/docs.ts
1570
+ import {AssetHashType, CfnOutput as Output4, RemovalPolicy as RemovalPolicy2, aws_lambda as lambda3} from "aws-cdk-lib";
1571
+ import {config as config8} from "@stacksjs/config";
1572
+ import {path as p5} from "@stacksjs/path";
1573
+ import {originRequestFunctionHash} from "@stacksjs/utils";
1574
+
1575
+ class DocsStack {
1576
+ originRequestFunction;
1577
+ constructor(scope, props) {
1578
+ const docsPrefix = config8.app.docMode ? "" : config8.docs.base;
1579
+ this.originRequestFunction = new lambda3.Function(scope, "OriginRequestFunction", {
1580
+ functionName: `${props.slug}-${props.appEnv}-origin-request-${props.timestamp}`,
1581
+ description: "The Stacks Origin Request function that prettifies URLs",
1582
+ runtime: lambda3.Runtime.NODEJS_18_X,
1583
+ handler: "dist/origin-request.handler",
1584
+ code: lambda3.Code.fromAsset(p5.corePath("cloud/dist.zip"), {
1585
+ assetHash: originRequestFunctionHash,
1586
+ assetHashType: AssetHashType.CUSTOM
1587
+ })
1588
+ });
1589
+ const cfnOriginRequestFunction = this.originRequestFunction.node.defaultChild;
1590
+ cfnOriginRequestFunction.applyRemovalPolicy(RemovalPolicy2.RETAIN);
1591
+ if (!config8.app.docMode && exports_storage.hasFiles(p5.projectPath("docs"))) {
1592
+ new Output4(scope, "DocsUrl", {
1593
+ value: `https://${props.domain}/${docsPrefix}`,
1594
+ description: "The URL of the deployed documentation"
1595
+ });
1596
+ }
1597
+ }
1598
+ }
1599
+
1600
+ // src/cloud/storage.ts
1601
+ import {RemovalPolicy as RemovalPolicy3, Tags, aws_backup as backup, aws_iam as iam2, aws_s3 as s32} from "aws-cdk-lib";
1602
+
1603
+ class StorageStack {
1604
+ publicBucket;
1605
+ privateBucket;
1606
+ logBucket;
1607
+ bucketPrefix;
1608
+ vault;
1609
+ backupPlan;
1610
+ backupRole;
1611
+ constructor(scope, props) {
1612
+ this.bucketPrefix = `${props.slug}-${props.appEnv}`;
1613
+ this.publicBucket = new s32.Bucket(scope, "PublicBucket", {
1614
+ bucketName: `${this.bucketPrefix}-public-${props.timestamp}`,
1615
+ versioned: true,
1616
+ autoDeleteObjects: true,
1617
+ removalPolicy: RemovalPolicy3.DESTROY,
1618
+ encryption: s32.BucketEncryption.S3_MANAGED
1619
+ });
1620
+ Tags.of(this.publicBucket).add("daily-backup", "true");
1621
+ this.privateBucket = new s32.Bucket(scope, "PrivateBucket", {
1622
+ bucketName: `${this.bucketPrefix}-private-${props.timestamp}`,
1623
+ versioned: true,
1624
+ removalPolicy: RemovalPolicy3.DESTROY,
1625
+ autoDeleteObjects: true,
1626
+ encryption: s32.BucketEncryption.S3_MANAGED,
1627
+ enforceSSL: true,
1628
+ publicReadAccess: false,
1629
+ blockPublicAccess: {
1630
+ blockPublicAcls: true,
1631
+ blockPublicPolicy: true,
1632
+ ignorePublicAcls: true,
1633
+ restrictPublicBuckets: true
1634
+ }
1635
+ });
1636
+ Tags.of(this.privateBucket).add("daily-backup", "true");
1637
+ this.logBucket = new s32.Bucket(scope, "LogsBucket", {
1638
+ bucketName: `${this.bucketPrefix}-logs-${props.timestamp}`,
1639
+ removalPolicy: RemovalPolicy3.RETAIN,
1640
+ blockPublicAccess: new s32.BlockPublicAccess({
1641
+ blockPublicAcls: false,
1642
+ ignorePublicAcls: true,
1643
+ blockPublicPolicy: true,
1644
+ restrictPublicBuckets: true
1645
+ }),
1646
+ objectOwnership: s32.ObjectOwnership.BUCKET_OWNER_PREFERRED
1647
+ });
1648
+ Tags.of(this.logBucket).add("daily-backup", "true");
1649
+ this.backupRole = this.createBackupRole(scope);
1650
+ this.vault = new backup.BackupVault(scope, "BackupVault", {
1651
+ backupVaultName: `${props.slug}-${props.appEnv}-daily-backup-vault`,
1652
+ encryptionKey: props.kmsKey,
1653
+ removalPolicy: RemovalPolicy3.DESTROY
1654
+ });
1655
+ this.backupPlan = backup.BackupPlan.daily35DayRetention(scope, "BackupPlan", this.vault);
1656
+ this.backupPlan.addSelection("Selection", {
1657
+ role: this.backupRole,
1658
+ resources: [backup.BackupResource.fromTag("daily-backup", "true")]
1659
+ });
1660
+ }
1661
+ createBackupRole(scope) {
1662
+ const backupRole = new iam2.Role(scope, "BackupRole", {
1663
+ assumedBy: new iam2.ServicePrincipal("backup.amazonaws.com")
1664
+ });
1665
+ backupRole.addToPolicy(new iam2.PolicyStatement({
1666
+ actions: [
1667
+ "s3:GetInventoryConfiguration",
1668
+ "s3:PutInventoryConfiguration",
1669
+ "s3:ListBucketVersions",
1670
+ "s3:ListBucket",
1671
+ "s3:GetBucketVersioning",
1672
+ "s3:GetBucketNotification",
1673
+ "s3:PutBucketNotification",
1674
+ "s3:GetBucketLocation",
1675
+ "s3:GetBucketTagging"
1676
+ ],
1677
+ resources: ["arn:aws:s3:::*"],
1678
+ sid: "S3BucketBackupPermissions"
1679
+ }));
1680
+ backupRole.addToPolicy(new iam2.PolicyStatement({
1681
+ actions: [
1682
+ "s3:GetObjectAcl",
1683
+ "s3:GetObject",
1684
+ "s3:GetObjectVersionTagging",
1685
+ "s3:GetObjectVersionAcl",
1686
+ "s3:GetObjectTagging",
1687
+ "s3:GetObjectVersion"
1688
+ ],
1689
+ resources: ["arn:aws:s3:::*/*"],
1690
+ sid: "S3ObjectBackupPermissions"
1691
+ }));
1692
+ backupRole.addToPolicy(new iam2.PolicyStatement({
1693
+ actions: ["s3:ListAllMyBuckets"],
1694
+ resources: ["*"],
1695
+ sid: "S3GlobalPermissions"
1696
+ }));
1697
+ backupRole.addToPolicy(new iam2.PolicyStatement({
1698
+ actions: ["kms:Decrypt", "kms:DescribeKey"],
1699
+ resources: ["*"],
1700
+ sid: "KMSBackupPermissions",
1701
+ conditions: {
1702
+ StringLike: {
1703
+ "kms:ViaService": "s3.*.amazonaws.com"
1704
+ }
1705
+ }
1706
+ }));
1707
+ backupRole.addToPolicy(new iam2.PolicyStatement({
1708
+ actions: [
1709
+ "events:DescribeRule",
1710
+ "events:EnableRule",
1711
+ "events:PutRule",
1712
+ "events:DeleteRule",
1713
+ "events:PutTargets",
1714
+ "events:RemoveTargets",
1715
+ "events:ListTargetsByRule",
1716
+ "events:DisableRule"
1717
+ ],
1718
+ resources: ["arn:aws:events:*:*:rule/AwsBackupManagedRule*"],
1719
+ sid: "EventsPermissions"
1720
+ }));
1721
+ backupRole.addToPolicy(new iam2.PolicyStatement({
1722
+ actions: ["cloudwatch:GetMetricData", "events:ListRules"],
1723
+ resources: ["*"],
1724
+ sid: "EventsMetricsGlobalPermissions"
1725
+ }));
1726
+ return backupRole;
1727
+ }
1728
+ }
1729
+
1730
+ // src/cloud/security.ts
1731
+ import {config as config10} from "@stacksjs/config";
1732
+ 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";
1733
+
1734
+ class SecurityStack {
1735
+ firewall;
1736
+ kmsKey;
1737
+ certificate;
1738
+ constructor(scope, props) {
1739
+ const firewallOptions = config10.cloud.firewall;
1740
+ if (!firewallOptions)
1741
+ throw new Error("No firewall options found in config");
1742
+ const options = {
1743
+ defaultAction: { allow: {} },
1744
+ scope: "CLOUDFRONT",
1745
+ visibilityConfig: {
1746
+ sampledRequestsEnabled: true,
1747
+ cloudWatchMetricsEnabled: true,
1748
+ metricName: "firewallMetric"
1749
+ },
1750
+ rules: this.getFirewallRules()
1751
+ };
1752
+ this.firewall = new wafv2.CfnWebACL(scope, "WebFirewall", options);
1753
+ Tags2.of(this.firewall).add("Name", "waf-cloudfront", { priority: 300 });
1754
+ Tags2.of(this.firewall).add("Purpose", "CloudFront", { priority: 300 });
1755
+ Tags2.of(this.firewall).add("CreatedBy", "CloudFormation", { priority: 300 });
1756
+ this.kmsKey = new kms.Key(scope, "EncryptionKey", {
1757
+ alias: "stacks-encryption-key",
1758
+ description: "KMS key for Stacks Cloud",
1759
+ enableKeyRotation: true,
1760
+ removalPolicy: RemovalPolicy4.DESTROY,
1761
+ pendingWindow: Duration4.days(30)
1762
+ });
1763
+ this.certificate = new acm.Certificate(scope, "Certificate", {
1764
+ domainName: props.domain,
1765
+ validation: acm.CertificateValidation.fromDns(props.zone),
1766
+ subjectAlternativeNames: [`www.${props.domain}`, `api.${props.domain}`]
1767
+ });
1768
+ }
1769
+ getFirewallRules() {
1770
+ const rules = [];
1771
+ const priorities = [];
1772
+ if (config10.security.firewall?.countryCodes?.length) {
1773
+ priorities.push(1);
1774
+ rules.push({
1775
+ name: "CountryRule",
1776
+ priority: priorities.length,
1777
+ statement: {
1778
+ geoMatchStatement: {
1779
+ countryCodes: config10.security.firewall.countryCodes
1780
+ }
1781
+ },
1782
+ action: {
1783
+ block: {}
1784
+ },
1785
+ visibilityConfig: {
1786
+ sampledRequestsEnabled: true,
1787
+ cloudWatchMetricsEnabled: true,
1788
+ metricName: "CountryRule"
1789
+ }
1790
+ });
1791
+ }
1792
+ if (config10.security.firewall?.ipAddresses?.length) {
1793
+ const ipSet = new wafv2.CfnIPSet(this, "IpSet", {
1794
+ name: "IpSet",
1795
+ description: "IP Set",
1796
+ scope: "CLOUDFRONT",
1797
+ addresses: config10.security.firewall.ipAddresses,
1798
+ ipAddressVersion: "IPV4"
1799
+ });
1800
+ priorities.push(1);
1801
+ rules.push({
1802
+ name: "IpAddressRule",
1803
+ priority: priorities.length,
1804
+ statement: {
1805
+ ipSetReferenceStatement: {
1806
+ arn: ipSet.attrArn
1807
+ }
1808
+ },
1809
+ action: {
1810
+ block: {}
1811
+ },
1812
+ visibilityConfig: {
1813
+ sampledRequestsEnabled: true,
1814
+ cloudWatchMetricsEnabled: true,
1815
+ metricName: "IpAddressRule"
1816
+ }
1817
+ });
1818
+ }
1819
+ if (config10.security.firewall?.httpHeaders?.length) {
1820
+ config10.security.firewall.httpHeaders.forEach((header, index) => {
1821
+ priorities.push(1);
1822
+ rules.push({
1823
+ name: `HttpHeaderRule${index}`,
1824
+ priority: priorities.length,
1825
+ statement: {
1826
+ byteMatchStatement: {
1827
+ fieldToMatch: {
1828
+ singleHeader: {
1829
+ name: header
1830
+ }
1831
+ },
1832
+ positionalConstraint: "EXACTLY",
1833
+ searchString: "true",
1834
+ textTransformations: [
1835
+ {
1836
+ priority: index,
1837
+ type: "NONE"
1838
+ }
1839
+ ]
1840
+ }
1841
+ },
1842
+ action: {
1843
+ block: {}
1844
+ },
1845
+ visibilityConfig: {
1846
+ sampledRequestsEnabled: true,
1847
+ cloudWatchMetricsEnabled: true,
1848
+ metricName: `HttpHeaderRule${index}`
1849
+ }
1850
+ });
1851
+ });
1852
+ }
1853
+ return rules;
1854
+ }
1855
+ }
1856
+
1857
+ // src/cloud/deployment.ts
1858
+ import {AssetHashType as AssetHashType2, aws_s3_deployment as s3deploy} from "aws-cdk-lib";
1859
+ import {config as config12} from "@stacksjs/config";
1860
+ import {websiteSourceHash} from "@stacksjs/utils";
1861
+
1862
+ class DeploymentStack {
1863
+ privateSource;
1864
+ docsSource;
1865
+ websiteSource;
1866
+ constructor(scope, props) {
1867
+ this.privateSource = "../../../private";
1868
+ this.docsSource = "../../docs/dist/";
1869
+ this.websiteSource = config12.app.docMode === true ? this.docsSource : "../../views/dist/";
1870
+ new s3deploy.BucketDeployment(scope, "Website", {
1871
+ sources: [s3deploy.Source.asset(this.websiteSource, {
1872
+ assetHash: websiteSourceHash,
1873
+ assetHashType: AssetHashType2.CUSTOM
1874
+ })],
1875
+ destinationBucket: props.publicBucket,
1876
+ distribution: props.cdn,
1877
+ distributionPaths: ["/*"]
1878
+ });
1879
+ new s3deploy.BucketDeployment(scope, "PrivateFiles", {
1880
+ sources: [s3deploy.Source.asset(this.privateSource)],
1881
+ destinationBucket: props.privateBucket
1882
+ });
1883
+ }
1884
+ }
1885
+
1886
+ // src/cloud/jump-box.ts
1887
+ import {CfnOutput as Output5, aws_ec2 as ec2, aws_iam as iam3} from "aws-cdk-lib";
1888
+
1889
+ class JumpBoxStack {
1890
+ jumpBox;
1891
+ constructor(scope, props) {
1892
+ const role = new iam3.Role(scope, "JumpBoxInstanceRole", {
1893
+ assumedBy: new iam3.ServicePrincipal("ec2.amazonaws.com"),
1894
+ managedPolicies: [
1895
+ iam3.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
1896
+ iam3.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy")
1897
+ ]
1898
+ });
1899
+ this.jumpBox = new ec2.Instance(scope, "JumpBox", {
1900
+ vpc: props.vpc,
1901
+ instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
1902
+ machineImage: new ec2.AmazonLinuxImage,
1903
+ role,
1904
+ userData: ec2.UserData.custom(`
1905
+ #!/bin/bash
1906
+ yum update -y
1907
+ yum install -y amazon-efs-utils
1908
+ yum install -y git
1909
+ yum install -y https://s3.us-east-1.amazonaws.com/amazon-ssm-us-east-1/latest/linux_amd64/amazon-ssm-agent.rpm
1910
+ mkdir /mnt/efs
1911
+ mount -t efs ${props.fileSystem.fileSystemId}:/ /mnt/efs
1912
+ git clone https://github.com/stacksjs/stacks.git /mnt/efs
1913
+ `)
1914
+ });
1915
+ new Output5(scope, "JumpBoxInstanceId", {
1916
+ value: this.jumpBox.instanceId,
1917
+ description: "The ID of the EC2 instance that can be used to SSH into the Stacks Cloud."
1918
+ });
1919
+ }
1920
+ }
1921
+
1922
+ // src/cloud/file-system.ts
1923
+ import {RemovalPolicy as RemovalPolicy5, aws_efs as efs} from "aws-cdk-lib";
1924
+
1925
+ class FileSystemStack {
1926
+ fileSystem;
1927
+ accessPoint;
1928
+ constructor(scope, props) {
1929
+ this.fileSystem = new efs.FileSystem(scope, "FileSystem", {
1930
+ fileSystemName: `${props.slug}-${props.appEnv}-efs`,
1931
+ vpc: props.vpc,
1932
+ removalPolicy: RemovalPolicy5.DESTROY,
1933
+ lifecyclePolicy: efs.LifecyclePolicy.AFTER_7_DAYS,
1934
+ performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
1935
+ throughputMode: efs.ThroughputMode.BURSTING,
1936
+ enableAutomaticBackups: true,
1937
+ encrypted: true
1938
+ });
1939
+ this.accessPoint = new efs.AccessPoint(scope, "FileSystemAccessPoint", {
1940
+ fileSystem: this.fileSystem,
1941
+ path: "/",
1942
+ posixUser: {
1943
+ uid: "1000",
1944
+ gid: "1000"
1945
+ }
1946
+ });
1947
+ }
1948
+ }
1949
+
1950
+ // src/cloud/network.ts
1951
+ import {aws_ec2 as ec22} from "aws-cdk-lib";
1952
+
1953
+ class NetworkStack {
1954
+ vpc;
1955
+ constructor(scope, props) {
1956
+ this.vpc = new ec22.Vpc(scope, "Network", {
1957
+ vpcName: `${props.slug}-${props.appEnv}-vpc`,
1958
+ ipAddresses: ec22.IpAddresses.cidr("10.0.0.0/16"),
1959
+ maxAzs: 3,
1960
+ natGateways: 0,
1961
+ subnetConfiguration: [
1962
+ {
1963
+ cidrMask: 21,
1964
+ name: `${props.slug}-${props.appEnv}-public-subnet-1`,
1965
+ subnetType: ec22.SubnetType.PUBLIC
1966
+ },
1967
+ {
1968
+ cidrMask: 21,
1969
+ name: `${props.slug}-${props.appEnv}-public-subnet-2`,
1970
+ subnetType: ec22.SubnetType.PUBLIC
1971
+ },
1972
+ {
1973
+ cidrMask: 21,
1974
+ name: `${props.slug}-${props.appEnv}-public-subnet-3`,
1975
+ subnetType: ec22.SubnetType.PUBLIC
1976
+ },
1977
+ {
1978
+ cidrMask: 21,
1979
+ name: `${props.slug}-${props.appEnv}-private-subnet-1`,
1980
+ subnetType: ec22.SubnetType.PRIVATE_ISOLATED
1981
+ },
1982
+ {
1983
+ cidrMask: 21,
1984
+ name: `${props.slug}-${props.appEnv}-private-subnet-2`,
1985
+ subnetType: ec22.SubnetType.PRIVATE_ISOLATED
1986
+ },
1987
+ {
1988
+ cidrMask: 21,
1989
+ name: `${props.slug}-${props.appEnv}-private-subnet-3`,
1990
+ subnetType: ec22.SubnetType.PRIVATE_ISOLATED
1991
+ }
1992
+ ]
1993
+ });
1994
+ }
1995
+ }
1996
+
1997
+ // src/cloud/redirects.ts
1998
+ import {config as config14} from "@stacksjs/config";
1999
+ import {RemovalPolicy as RemovalPolicy6, aws_route53 as route533, aws_s3 as s33} from "aws-cdk-lib";
2000
+
2001
+ class RedirectsStack {
2002
+ redirectZones = [];
2003
+ constructor(scope, props) {
2004
+ config14.dns.redirects?.forEach((redirect) => {
2005
+ const slug2 = redirect.split(".").map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
2006
+ const hostedZone = route533.HostedZone.fromLookup(scope, "HostedZone", { domainName: redirect });
2007
+ const redirectBucket = new s33.Bucket(scope, `RedirectBucket${slug2}`, {
2008
+ bucketName: `${redirect}-redirect`,
2009
+ websiteRedirect: {
2010
+ hostName: props.domain,
2011
+ protocol: s33.RedirectProtocol.HTTPS
2012
+ },
2013
+ removalPolicy: RemovalPolicy6.DESTROY,
2014
+ autoDeleteObjects: true
2015
+ });
2016
+ new route533.CnameRecord(scope, `RedirectRecord${slug2}`, {
2017
+ zone: hostedZone,
2018
+ recordName: "redirect",
2019
+ domainName: redirectBucket.bucketWebsiteDomainName
2020
+ });
2021
+ });
2022
+ config14.dns.redirects?.forEach((redirect) => {
2023
+ const slug2 = redirect.split(".").map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
2024
+ const hostedZone = route533.HostedZone.fromLookup(scope, `RedirectHostedZone${slug2}`, { domainName: redirect });
2025
+ this.redirectZones.push(hostedZone);
2026
+ });
2027
+ }
2028
+ }
2029
+
2030
+ // src/cloud/email.ts
2031
+ 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";
2032
+ import {config as config16} from "@stacksjs/config";
2033
+
2034
+ class EmailStack {
2035
+ emailBucket;
2036
+ constructor(scope, props) {
2037
+ const bucketPrefix = `${props.slug}-${props.appEnv}`;
2038
+ this.emailBucket = new s34.Bucket(scope, "EmailBucket", {
2039
+ bucketName: `${bucketPrefix}-email-${props.timestamp}`,
2040
+ versioned: true,
2041
+ removalPolicy: RemovalPolicy7.DESTROY,
2042
+ autoDeleteObjects: true,
2043
+ encryption: s34.BucketEncryption.S3_MANAGED,
2044
+ lifecycleRules: [
2045
+ {
2046
+ id: "24h",
2047
+ enabled: true,
2048
+ expiration: Duration5.days(1),
2049
+ noncurrentVersionExpiration: Duration5.days(1),
2050
+ prefix: "today/"
2051
+ },
2052
+ {
2053
+ id: "Intelligent transition for Inbox",
2054
+ enabled: true,
2055
+ prefix: "inbox/",
2056
+ transitions: [
2057
+ {
2058
+ storageClass: s34.StorageClass.INTELLIGENT_TIERING,
2059
+ transitionAfter: Duration5.days(0)
2060
+ }
2061
+ ]
2062
+ },
2063
+ {
2064
+ id: "Intelligent transition for Sent",
2065
+ enabled: true,
2066
+ prefix: "sent/",
2067
+ transitions: [
2068
+ {
2069
+ storageClass: s34.StorageClass.INTELLIGENT_TIERING,
2070
+ transitionAfter: Duration5.days(0)
2071
+ }
2072
+ ]
2073
+ }
2074
+ ]
2075
+ });
2076
+ Tags3.of(this.emailBucket).add("daily-backup", "true");
2077
+ const sesPrincipal = new iam4.ServicePrincipal("ses.amazonaws.com");
2078
+ const ruleSetName = `${props.slug}-${props.appEnv}-email-receipt-rule-set`;
2079
+ const receiptRuleName = `${props.slug}-${props.appEnv}-email-receipt-rule`;
2080
+ const ruleSet = new ses.CfnReceiptRuleSet(scope, "SESReceiptRuleSet", {
2081
+ ruleSetName
2082
+ });
2083
+ this.emailBucket.addToResourcePolicy(new iam4.PolicyStatement({
2084
+ sid: "AllowSESPuts",
2085
+ effect: iam4.Effect.ALLOW,
2086
+ principals: [sesPrincipal],
2087
+ actions: [
2088
+ "s3:PutObject"
2089
+ ],
2090
+ resources: [
2091
+ `${this.emailBucket.bucketArn}/*`
2092
+ ],
2093
+ conditions: {
2094
+ StringEquals: {
2095
+ "aws:SourceAccount": Stack.of(scope).account
2096
+ },
2097
+ ArnLike: {
2098
+ "aws:SourceArn": `arn:aws:ses:${Stack.of(scope).region}:${Stack.of(scope).account}:receipt-rule-set/${ruleSetName}:receipt-rule/${receiptRuleName}`
2099
+ }
2100
+ }
2101
+ }));
2102
+ const receiptRule = new ses.CfnReceiptRule(scope, "SESReceiptRule", {
2103
+ ruleSetName: ruleSet.ref,
2104
+ rule: {
2105
+ name: receiptRuleName,
2106
+ enabled: true,
2107
+ actions: [
2108
+ {
2109
+ s3Action: {
2110
+ bucketName: this.emailBucket.bucketName,
2111
+ objectKeyPrefix: "tmp/email_in/"
2112
+ }
2113
+ }
2114
+ ],
2115
+ recipients: config16.email.mailboxes || [],
2116
+ scanEnabled: config16.email.server?.scan || true,
2117
+ tlsPolicy: "Require"
2118
+ }
2119
+ });
2120
+ receiptRule.node.addDependency(this.emailBucket);
2121
+ const iamGroup = new iam4.Group(scope, "IAMGroup", {
2122
+ groupName: `${props.slug}-${props.appEnv}-email-management-s3-group`
2123
+ });
2124
+ const listBucketsPolicyStatement = new iam4.PolicyStatement({
2125
+ effect: iam4.Effect.ALLOW,
2126
+ actions: ["s3:ListAllMyBuckets"],
2127
+ resources: ["*"]
2128
+ });
2129
+ const policyStatement = new iam4.PolicyStatement({
2130
+ effect: iam4.Effect.ALLOW,
2131
+ actions: [
2132
+ "s3:ListBucket",
2133
+ "s3:GetObject",
2134
+ "s3:PutObject",
2135
+ "s3:DeleteObject",
2136
+ "s3:GetObjectAcl",
2137
+ "s3:GetObjectVersionAcl",
2138
+ "s3:PutObjectAcl",
2139
+ "s3:PutObjectVersionAcl"
2140
+ ],
2141
+ resources: [
2142
+ this.emailBucket.bucketArn,
2143
+ `${this.emailBucket.bucketArn}/*`
2144
+ ]
2145
+ });
2146
+ const policy = new iam4.Policy(scope, "EmailAccessPolicy", {
2147
+ policyName: `${props.slug}-${props.appEnv}-email-management-s3-policy`,
2148
+ statements: [policyStatement, listBucketsPolicyStatement]
2149
+ });
2150
+ iamGroup.attachInlinePolicy(policy);
2151
+ const sesIdentity = new ses.CfnEmailIdentity(scope, "DomainIdentity", {
2152
+ emailIdentity: props.domain,
2153
+ dkimSigningAttributes: {
2154
+ nextSigningKeyLength: "RSA_2048_BIT"
2155
+ },
2156
+ dkimAttributes: {
2157
+ signingEnabled: true
2158
+ },
2159
+ mailFromAttributes: {
2160
+ behaviorOnMxFailure: "USE_DEFAULT_VALUE",
2161
+ mailFromDomain: `mail.${props.domain}`
2162
+ },
2163
+ feedbackAttributes: {
2164
+ emailForwardingEnabled: true
2165
+ }
2166
+ });
2167
+ new route534.CfnRecordSet(scope, "DkimRecord1", {
2168
+ hostedZoneName: `${props.zone.zoneName}.`,
2169
+ name: sesIdentity.attrDkimDnsTokenName1,
2170
+ type: "CNAME",
2171
+ resourceRecords: [sesIdentity.attrDkimDnsTokenValue1],
2172
+ ttl: "1800"
2173
+ });
2174
+ new route534.CfnRecordSet(scope, "DkimRecord2", {
2175
+ hostedZoneName: `${props.zone.zoneName}.`,
2176
+ name: sesIdentity.attrDkimDnsTokenName2,
2177
+ type: "CNAME",
2178
+ resourceRecords: [sesIdentity.attrDkimDnsTokenValue2],
2179
+ ttl: "1800"
2180
+ });
2181
+ new route534.CfnRecordSet(scope, "DkimRecord3", {
2182
+ hostedZoneName: `${props.zone.zoneName}.`,
2183
+ name: sesIdentity.attrDkimDnsTokenName3,
2184
+ type: "CNAME",
2185
+ resourceRecords: [sesIdentity.attrDkimDnsTokenValue3],
2186
+ ttl: "1800"
2187
+ });
2188
+ new route534.MxRecord(scope, "MxRecord", {
2189
+ zone: props.zone,
2190
+ recordName: "mail",
2191
+ values: [{
2192
+ priority: 10,
2193
+ hostName: "feedback-smtp.us-east-1.amazonses.com"
2194
+ }]
2195
+ });
2196
+ new route534.TxtRecord(scope, "TxtSpfRecord", {
2197
+ zone: props.zone,
2198
+ recordName: "mail",
2199
+ values: ["v=spf1 include:amazonses.com ~all"]
2200
+ });
2201
+ new route534.TxtRecord(scope, "TxtDmarcRecord", {
2202
+ zone: props.zone,
2203
+ recordName: "_dmarc",
2204
+ values: [`v=DMARC1;p=quarantine;pct=25;rua=mailto:dmarcreports@${props.domain}`]
2205
+ });
2206
+ const lambdaEmailOutboundRole = new iam4.Role(scope, "LambdaEmailOutboundRole", {
2207
+ roleName: `${props.slug}-${props.appEnv}-email-outbound`,
2208
+ assumedBy: new iam4.ServicePrincipal("lambda.amazonaws.com"),
2209
+ managedPolicies: [
2210
+ iam4.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
2211
+ ]
2212
+ });
2213
+ const lambdaEmailOutbound = new lambda4.Function(scope, "LambdaEmailOutbound", {
2214
+ functionName: `${props.slug}-${props.appEnv}-email-outbound`,
2215
+ description: "Take the JSON and convert it in to an raw email.",
2216
+ code: lambda4.Code.fromInline("exports.handler = async (event) => {return true;};"),
2217
+ handler: "index.handler",
2218
+ memorySize: 256,
2219
+ runtime: lambda4.Runtime.NODEJS_18_X,
2220
+ timeout: Duration5.seconds(60),
2221
+ environment: {
2222
+ BUCKET: this.emailBucket.bucketName
2223
+ },
2224
+ role: lambdaEmailOutboundRole
2225
+ });
2226
+ lambdaEmailOutboundRole.addToPolicy(policyStatement);
2227
+ const sesPolicyStatement = new iam4.PolicyStatement({
2228
+ effect: iam4.Effect.ALLOW,
2229
+ actions: ["ses:SendRawEmail"],
2230
+ resources: ["*"]
2231
+ });
2232
+ lambdaEmailOutboundRole.addToPolicy(sesPolicyStatement);
2233
+ const lambdaEmailInboundRole = new iam4.Role(scope, "LambdaEmailInboundRole", {
2234
+ roleName: `${props.slug}-${props.appEnv}-email-inbound`,
2235
+ assumedBy: new iam4.ServicePrincipal("lambda.amazonaws.com"),
2236
+ managedPolicies: [
2237
+ iam4.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
2238
+ ]
2239
+ });
2240
+ const lambdaEmailInbound = new lambda4.Function(scope, "LambdaEmailInbound", {
2241
+ functionName: `${props.slug}-${props.appEnv}-email-inbound`,
2242
+ description: "This Lambda organizes all the incoming emails based on the From and To field.",
2243
+ code: lambda4.Code.fromInline("exports.handler = async (event) => {return true;};"),
2244
+ handler: "index.handler",
2245
+ memorySize: 256,
2246
+ role: lambdaEmailInboundRole,
2247
+ runtime: lambda4.Runtime.NODEJS_18_X,
2248
+ timeout: Duration5.seconds(60),
2249
+ environment: {
2250
+ BUCKET: this.emailBucket.bucketName
2251
+ }
2252
+ });
2253
+ new lambda4.CfnPermission(scope, "S3InboundPermission", {
2254
+ action: "lambda:InvokeFunction",
2255
+ functionName: lambdaEmailInbound.functionName,
2256
+ principal: "s3.amazonaws.com"
2257
+ });
2258
+ const inboundS3PolicyStatement = new iam4.PolicyStatement({
2259
+ effect: iam4.Effect.ALLOW,
2260
+ actions: ["s3:*"],
2261
+ resources: [
2262
+ this.emailBucket.bucketArn,
2263
+ `${this.emailBucket.bucketArn}/*`
2264
+ ]
2265
+ });
2266
+ lambdaEmailInboundRole.addToPolicy(inboundS3PolicyStatement);
2267
+ const sesInboundPolicyStatement = new iam4.PolicyStatement({
2268
+ effect: iam4.Effect.ALLOW,
2269
+ actions: ["ses:ListIdentities"],
2270
+ resources: ["*"]
2271
+ });
2272
+ lambdaEmailInboundRole.addToPolicy(sesInboundPolicyStatement);
2273
+ const lambdaEmailConverterRole = new iam4.Role(scope, "LambdaEmailConverterRole", {
2274
+ roleName: `${props.slug}-${props.appEnv}-email-converter`,
2275
+ assumedBy: new iam4.ServicePrincipal("lambda.amazonaws.com"),
2276
+ managedPolicies: [
2277
+ iam4.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
2278
+ ]
2279
+ });
2280
+ const lambdaEmailConverter = new lambda4.Function(scope, "LambdaEmailConverter", {
2281
+ functionName: `${props.slug}-${props.appEnv}-email-converter`,
2282
+ description: "This Lambda converts raw emails files in to HTML and text.",
2283
+ code: lambda4.Code.fromInline('exports.handler = async (event) => {console.log("hello world email converter");return true;};'),
2284
+ handler: "index.handler",
2285
+ memorySize: 256,
2286
+ role: lambdaEmailConverterRole,
2287
+ runtime: lambda4.Runtime.NODEJS_18_X,
2288
+ timeout: Duration5.seconds(60),
2289
+ environment: {
2290
+ BUCKET: this.emailBucket.bucketName
2291
+ }
2292
+ });
2293
+ const converterS3PolicyStatement = new iam4.PolicyStatement({
2294
+ effect: iam4.Effect.ALLOW,
2295
+ actions: ["s3:*"],
2296
+ resources: [
2297
+ this.emailBucket.bucketArn,
2298
+ `${this.emailBucket.bucketArn}/*`
2299
+ ]
2300
+ });
2301
+ new lambda4.CfnPermission(scope, "S3ConverterPermission", {
2302
+ action: "lambda:InvokeFunction",
2303
+ functionName: lambdaEmailConverter.functionName,
2304
+ principal: "s3.amazonaws.com"
2305
+ });
2306
+ lambdaEmailConverterRole.addToPolicy(converterS3PolicyStatement);
2307
+ this.emailBucket.addEventNotification(s34.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(lambdaEmailInbound), { prefix: "tmp/email_in/" });
2308
+ this.emailBucket.addEventNotification(s34.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(lambdaEmailOutbound), { prefix: "tmp/email_out/json/" });
2309
+ this.emailBucket.addEventNotification(s34.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: "sent/" });
2310
+ this.emailBucket.addEventNotification(s34.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: "inbox/" });
2311
+ this.emailBucket.addEventNotification(s34.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: "today/" });
2312
+ }
2313
+ }
2314
+
2315
+ // src/cloud/permissions.ts
2316
+ import {SecretValue, aws_iam as iam5} from "aws-cdk-lib";
2317
+ import {config as config18} from "@stacksjs/config";
2318
+ import {string} from "@stacksjs/strings";
2319
+ import {env as env4} from "@stacksjs/env";
2320
+
2321
+ class PermissionsStack {
2322
+ constructor(scope) {
2323
+ const teamName = config18.team.name;
2324
+ const users = config18.team.members;
2325
+ const password = env4.AWS_DEFAULT_PASSWORD || string.random();
2326
+ for (const name in users) {
2327
+ const id = `User${string.pascalCase(teamName)}${string.pascalCase(name)}`;
2328
+ const userName = string.slug(`${teamName}-${name}`);
2329
+ const user = new iam5.User(scope, id, {
2330
+ userName,
2331
+ password: SecretValue.unsafePlainText(password),
2332
+ passwordResetRequired: true
2333
+ });
2334
+ user.addManagedPolicy(iam5.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess"));
2335
+ }
2336
+ }
2337
+ }
2338
+
2339
+ // src/cloud/compute.ts
2340
+ import {Duration as Duration6, CfnOutput as Output6, aws_lambda as lambda5, aws_logs as logs, aws_secretsmanager as secretsmanager} from "aws-cdk-lib";
2341
+ import {path as p6} from "@stacksjs/path";
2342
+ import {env as env6} from "@stacksjs/env";
2343
+
2344
+ class ComputeStack {
2345
+ apiServer;
2346
+ apiServerUrl;
2347
+ constructor(scope, props) {
2348
+ const vpc = props.vpc;
2349
+ const fileSystem = props.fileSystem;
2350
+ if (!fileSystem)
2351
+ throw new Error("The file system is missing. Please make sure it was created properly.");
2352
+ this.apiServer = new lambda5.Function(scope, "WebServer", {
2353
+ functionName: `${props.slug}-${props.appEnv}-web-server`,
2354
+ description: "The web server for the Stacks application",
2355
+ code: lambda5.Code.fromAssetImage(p6.frameworkPath("server")),
2356
+ handler: lambda5.Handler.FROM_IMAGE,
2357
+ runtime: lambda5.Runtime.FROM_IMAGE,
2358
+ vpc,
2359
+ memorySize: 512,
2360
+ timeout: Duration6.minutes(5),
2361
+ logRetention: logs.RetentionDays.ONE_WEEK,
2362
+ architecture: lambda5.Architecture.ARM_64
2363
+ });
2364
+ 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", "_"];
2365
+ keysToRemove.forEach((key) => delete env6[key]);
2366
+ const secrets = new secretsmanager.Secret(scope, "StacksSecrets", {
2367
+ secretName: `${props.slug}-${props.appEnv}-secrets`,
2368
+ description: "Secrets for the Stacks application",
2369
+ generateSecretString: {
2370
+ secretStringTemplate: JSON.stringify(env6),
2371
+ generateStringKey: Object.keys(env6).join(",").length.toString()
2372
+ }
2373
+ });
2374
+ secrets.grantRead(this.apiServer);
2375
+ this.apiServer.addEnvironment("SECRETS_ARN", secrets.secretArn);
2376
+ this.apiServerUrl = new lambda5.FunctionUrl(scope, "StacksServerUrl", {
2377
+ function: this.apiServer,
2378
+ authType: lambda5.FunctionUrlAuthType.NONE,
2379
+ cors: {
2380
+ allowedOrigins: ["*"]
2381
+ }
2382
+ });
2383
+ const apiPrefix = "api";
2384
+ new Output6(scope, "ApiUrl", {
2385
+ value: `https://${props.domain}/${apiPrefix}`,
2386
+ description: "The URL of the deployed application"
2387
+ });
2388
+ new Output6(scope, "ApiVanityUrl", {
2389
+ value: this.apiServerUrl.url,
2390
+ description: "The Vanity URL of the deployed application"
2391
+ });
2392
+ }
2393
+ }
2394
+
2395
+ // src/cloud/index.ts
2396
+ class Cloud extends Stack2 {
2397
+ constructor(scope, id, props) {
2398
+ super(scope, id, props);
2399
+ const dns2 = new DnsStack(this, props);
2400
+ const security2 = new SecurityStack(this, {
2401
+ ...props,
2402
+ zone: dns2.zone
2403
+ });
2404
+ const storage4 = new StorageStack(this, {
2405
+ ...props,
2406
+ kmsKey: security2.kmsKey
2407
+ });
2408
+ const network2 = new NetworkStack(this, props);
2409
+ const fileSystem = new FileSystemStack(this, {
2410
+ ...props,
2411
+ vpc: network2.vpc
2412
+ });
2413
+ new JumpBoxStack(this, {
2414
+ ...props,
2415
+ vpc: network2.vpc,
2416
+ fileSystem: fileSystem.fileSystem
2417
+ });
2418
+ const docs2 = new DocsStack(this, props);
2419
+ new EmailStack(this, {
2420
+ ...props,
2421
+ zone: dns2.zone
2422
+ });
2423
+ new RedirectsStack(this, props);
2424
+ new PermissionsStack(this);
2425
+ const api = new ComputeStack(this, {
2426
+ ...props,
2427
+ vpc: network2.vpc,
2428
+ fileSystem: fileSystem.fileSystem,
2429
+ zone: dns2.zone,
2430
+ certificate: security2.certificate
2431
+ });
2432
+ const ai2 = new AiStack(this, props);
2433
+ const cli4 = new CliStack(this, props);
2434
+ const cdn2 = new CdnStack(this, {
2435
+ ...props,
2436
+ publicBucket: storage4.publicBucket,
2437
+ logBucket: storage4.logBucket,
2438
+ certificate: security2.certificate,
2439
+ firewall: security2.firewall,
2440
+ originRequestFunction: docs2.originRequestFunction,
2441
+ zone: dns2.zone,
2442
+ webServer: api.apiServer,
2443
+ webServerUrl: api.apiServerUrl,
2444
+ cliSetupUrl: cli4.cliSetupUrl,
2445
+ askAiUrl: ai2.askAiUrl,
2446
+ summarizeAiUrl: ai2.summarizeAiUrl
2447
+ });
2448
+ new DeploymentStack(this, {
2449
+ ...props,
2450
+ publicBucket: storage4.publicBucket,
2451
+ privateBucket: storage4.privateBucket,
2452
+ cdn: cdn2.distribution
2453
+ });
2454
+ }
2455
+ }
2456
+ // /home/runner/work/stacks/stacks/storage/framework/core/router/src/middleware.ts
2457
+ import {appPath} from "@stacksjs/path";
2458
+ async function importMiddlewares(directory) {
2459
+ return [directory];
2460
+ }
2461
+ var middlewares = await importMiddlewares(appPath("middleware"));
2462
+ // /home/runner/work/stacks/stacks/storage/framework/core/router/src/request.ts
2463
+ class Request {
2464
+ query = {};
2465
+ params = null;
2466
+ addQuery(url) {
2467
+ this.query = Object.fromEntries(url.searchParams);
2468
+ }
2469
+ get(element) {
2470
+ return this.query[element];
2471
+ }
2472
+ all() {
2473
+ return this.query;
2474
+ }
2475
+ has(element) {
2476
+ return element in this.query;
2477
+ }
2478
+ isEmpty() {
2479
+ return Object.keys(this.query).length === 0;
2480
+ }
2481
+ extractParamsFromRoute(routePattern, pathname) {
2482
+ const pattern = new RegExp(`^${routePattern.replace(/:(\w+)/g, (match2, paramName) => `(?<${paramName}>\\w+)`)}\$`);
2483
+ const match = pattern.exec(pathname);
2484
+ if (match?.groups)
2485
+ this.params = match?.groups;
2486
+ }
2487
+ getParams(key) {
2488
+ return this.params ? this.params[key] || null : null;
2489
+ }
2490
+ }
2491
+ var request = new Request;
2492
+ // /home/runner/work/stacks/stacks/storage/framework/core/router/src/server.ts
2493
+ import {extname} from "path";
2494
+ import {URL} from "url";
2495
+ import {localUrl} from "@stacksjs/config";
2496
+ async function serverResponse(req) {
2497
+ console.log("serverResponse", req);
2498
+ const routesList = await route.getRoutes();
2499
+ const url = new URL(req.url);
2500
+ const foundRoute = routesList.find((route2) => {
2501
+ const pattern = new RegExp(`^${route2.uri.replace(/:\w+/g, "\\w+")}\$`);
2502
+ return pattern.test(url.pathname);
2503
+ });
2504
+ if (!foundRoute)
2505
+ return new Response("Not found", { status: 404 });
2506
+ addRouteParamsAndQuery(url, foundRoute);
2507
+ executeMiddleware(foundRoute);
2508
+ return execute(foundRoute, req, { statusCode: foundRoute?.statusCode });
2509
+ }
2510
+ var addRouteParamsAndQuery = function(url, route2) {
2511
+ if (!isObjectNotEmpty(url.searchParams))
2512
+ request.addQuery(url);
2513
+ request.extractParamsFromRoute(route2.uri, url.pathname);
2514
+ };
2515
+ var executeMiddleware = function(route2) {
2516
+ const { middleware: middleware2 = null } = route2;
2517
+ if (middleware2 && middlewares && isObjectNotEmpty(middlewares)) {
2518
+ if (isString(middleware2)) {
2519
+ const middlewareItem = middlewares.find((middlewareItem2) => {
2520
+ return middlewareItem2.name === middleware2;
2521
+ });
2522
+ if (middlewareItem)
2523
+ middlewareItem.handle();
2524
+ } else {
2525
+ middleware2.forEach((m) => {
2526
+ const middlewareItem = middlewares.find((middlewareItem2) => {
2527
+ return middlewareItem2.name === m;
2528
+ });
2529
+ if (middlewareItem)
2530
+ middlewareItem.handle();
2531
+ });
2532
+ }
2533
+ }
2534
+ };
2535
+ var execute = function(route2, request3, { statusCode }) {
2536
+ if (!statusCode)
2537
+ statusCode = 200;
2538
+ if (route2?.method === "GET" && (statusCode === 301 || statusCode === 302)) {
2539
+ const callback = String(route2.callback);
2540
+ const response = Response.redirect(callback, statusCode);
2541
+ return noCache(response);
2542
+ }
2543
+ if (route2?.method !== request3.method)
2544
+ return new Response("Method not allowed", { status: 405 });
2545
+ if (isString(route2.callback) && extname(route2.callback) === ".html") {
2546
+ try {
2547
+ const fileContent = Bun.file(route2.callback);
2548
+ return new Response(fileContent, { headers: { "Content-Type": "text/html" } });
2549
+ } catch (error) {
2550
+ return new Response("Error reading the HTML file", { status: 500 });
2551
+ }
2552
+ }
2553
+ if (isString(route2.callback))
2554
+ return new Response(route2.callback);
2555
+ if (isFunction(route2.callback)) {
2556
+ const result = route2.callback();
2557
+ return new Response(JSON.stringify(result));
2558
+ }
2559
+ if (isObject(route2.callback))
2560
+ return new Response(JSON.stringify(route2.callback));
2561
+ return new Response("Unknown callback type.", { status: 500 });
2562
+ };
2563
+ var noCache = function(response) {
2564
+ response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
2565
+ response.headers.set("Pragma", "no-cache");
2566
+ response.headers.set("Expires", "0");
2567
+ return response;
2568
+ };
2569
+ var isString = function(val) {
2570
+ return typeof val === "string";
2571
+ };
2572
+ var isObjectNotEmpty = function(obj) {
2573
+ return Object.keys(obj).length > 0;
2574
+ };
2575
+ var isFunction = function(val) {
2576
+ return typeof val === "function";
2577
+ };
2578
+ var isObject = function(val) {
2579
+ return val !== null && typeof val === "object" && !Array.isArray(val);
2580
+ };
2581
+ // /home/runner/work/stacks/stacks/storage/framework/core/router/src/router.ts
2582
+ import {projectPath} from "@stacksjs/path";
2583
+
2584
+ class Router {
2585
+ routes = [];
2586
+ addRoute(method, uri, callback, statusCode) {
2587
+ const name = uri.replace(/\//g, ".").replace(/:/g, "");
2588
+ const pattern = new RegExp(`^${uri.replace(/:[a-zA-Z]+/g, (_match) => {
2589
+ return "([a-zA-Z0-9-]+)";
2590
+ })}\$`);
2591
+ let routeCallback;
2592
+ if (typeof callback === "string" || typeof callback === "object") {
2593
+ routeCallback = () => callback;
2594
+ } else {
2595
+ routeCallback = callback;
2596
+ }
2597
+ this.routes.push({
2598
+ name,
2599
+ method,
2600
+ url: uri,
2601
+ uri,
2602
+ callback: routeCallback,
2603
+ pattern,
2604
+ statusCode,
2605
+ paramNames: []
2606
+ });
2607
+ }
2608
+ get(path14, callback) {
2609
+ this.addRoute("GET", path14, callback, 200);
2610
+ return this;
2611
+ }
2612
+ post(path14, callback) {
2613
+ this.addRoute("POST", path14, callback, 201);
2614
+ return this;
2615
+ }
2616
+ view(path14, callback) {
2617
+ this.addRoute("GET", path14, callback, 200);
2618
+ return this;
2619
+ }
2620
+ redirect(path14, callback, _status) {
2621
+ this.addRoute("GET", path14, callback, 302);
2622
+ return this;
2623
+ }
2624
+ delete(path14, callback) {
2625
+ this.addRoute("DELETE", path14, callback, 204);
2626
+ return this;
2627
+ }
2628
+ patch(path14, callback) {
2629
+ this.addRoute("PATCH", path14, callback, 202);
2630
+ return this;
2631
+ }
2632
+ put(path14, callback) {
2633
+ this.addRoute("PUT", path14, callback, 202);
2634
+ return this;
2635
+ }
2636
+ group(options, callback) {
2637
+ let cb;
2638
+ if (typeof options === "function") {
2639
+ cb = options;
2640
+ options = {};
2641
+ } else {
2642
+ if (!callback)
2643
+ throw new Error("Missing callback function for route group.");
2644
+ cb = callback;
2645
+ }
2646
+ const { prefix = "", middleware: middleware2 = [] } = options;
2647
+ const originalRoutes = this.routes;
2648
+ this.routes = [];
2649
+ cb();
2650
+ this.routes.forEach((r) => {
2651
+ r.uri = `${prefix}${r.uri}`;
2652
+ if (middleware2.length)
2653
+ r.middleware = middleware2;
2654
+ originalRoutes.push(r);
2655
+ return this;
2656
+ });
2657
+ this.routes = originalRoutes;
2658
+ return this;
2659
+ }
2660
+ name(name) {
2661
+ this.routes[this.routes.length - 1].name = name;
2662
+ return this;
2663
+ }
2664
+ middleware(middleware2) {
2665
+ this.routes[this.routes.length - 1].middleware = middleware2;
2666
+ return this;
2667
+ }
2668
+ prefix(prefix) {
2669
+ this.routes[this.routes.length - 1].prefix = prefix;
2670
+ return this;
2671
+ }
2672
+ async getRoutes() {
2673
+ await import(projectPath("routes/api.ts"));
2674
+ return this.routes;
2675
+ }
2676
+ }
2677
+ var route = new Router;
2678
+ // src/runtime/server.ts
2679
+ var server_default = {
2680
+ async fetch(request4, server2) {
2681
+ console.log("Request", {
2682
+ url: request4.url,
2683
+ method: request4.method,
2684
+ headers: request4.headers.toJSON(),
2685
+ body: request4.body ? await request4.text() : null
2686
+ });
2687
+ if (server2.upgrade(request4)) {
2688
+ console.log("WebSocket upgraded");
2689
+ return;
2690
+ }
2691
+ return serverResponse(request4);
2692
+ },
2693
+ websocket: {}
2694
+ };
2695
+ export {
2696
+ server_default as server,
2697
+ purchaseDomain,
2698
+ isFirstDeployment,
2699
+ isFailedState,
2700
+ hasBeenDeployed,
2701
+ getSecurityGroupId,
2702
+ getSecurityGroupFromInstanceId,
2703
+ getOrCreateTimestamp,
2704
+ getJumpBoxSecurityGroupName,
2705
+ getJumpBoxInstanceProfileName,
2706
+ getJumpBoxInstanceId,
2707
+ deleteStacksFunctions,
2708
+ deleteStacksBuckets,
2709
+ deleteParameterStore,
2710
+ deleteLogGroups,
2711
+ deleteJumpBox,
2712
+ deleteIamUsers,
2713
+ deleteEc2Instance,
2714
+ deleteCdkRemnants,
2715
+ addJumpBox,
2716
+ InstanceType,
2717
+ Cloud
2718
+ };