@stacksjs/cloud 0.58.48 → 0.58.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +144 -60
- package/package.json +34 -33
- package/src/cloud/ai.ts +94 -0
- package/src/cloud/aws-sdk-layer/nodejs/package-lock.json +386 -0
- package/src/cloud/aws-sdk-layer/nodejs/package.json +15 -0
- package/src/cloud/cache.ts +0 -0
- package/src/cloud/cdn.ts +374 -0
- package/src/cloud/cli.ts +45 -0
- package/src/cloud/compute.ts +202 -0
- package/src/cloud/dashboard.ts +85 -0
- package/src/cloud/database.ts +0 -0
- package/src/cloud/deployment.ts +41 -0
- package/src/cloud/dns.ts +34 -0
- package/src/cloud/docs.ts +51 -0
- package/src/cloud/email.ts +339 -0
- package/src/cloud/file-system.ts +35 -0
- package/src/cloud/index.ts +103 -0
- package/src/cloud/jump-box.ts +48 -0
- package/src/cloud/lambda/ask/index.js +35 -0
- package/src/cloud/lambda/cli-setup/index.js +67 -0
- package/src/cloud/lambda/summarize/index.js +35 -0
- package/src/cloud/network.ts +32 -0
- package/src/cloud/package/README.md +59 -0
- package/src/cloud/package/package.json +63 -0
- package/src/cloud/permissions.ts +33 -0
- package/src/cloud/queue.ts +0 -0
- package/src/cloud/redirects.ts +45 -0
- package/src/cloud/router-layer/nodejs/package.json +15 -0
- package/src/cloud/search-engine.ts +108 -0
- package/src/cloud/security.ts +259 -0
- package/src/cloud/storage.ts +168 -0
- package/src/edge/origin-request.ts +49 -0
- package/src/helpers.ts +597 -0
- package/src/index.ts +3 -0
- package/src/runtime/README.md +116 -0
- package/src/runtime/bootstrap +3 -0
- package/src/runtime/example/lambda.ts +36 -0
- package/src/runtime/runtime.ts +830 -0
- package/src/runtime/scripts/build-layer.ts +104 -0
- package/src/runtime/scripts/publish-layer.ts +110 -0
- package/src/runtime/server.ts +39 -0
- package/src/types.ts +28 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/* eslint-disable no-new */
|
|
2
|
+
// TODO: finish this cloudwatch dashboard
|
|
3
|
+
// import type { aws_lambda as lambda } from 'aws-cdk-lib'
|
|
4
|
+
import { Aws, CfnOutput as Output, aws_cloudwatch as cloudwatch } from 'aws-cdk-lib'
|
|
5
|
+
import type { Construct } from 'constructs'
|
|
6
|
+
import type { NestedCloudProps } from '../types'
|
|
7
|
+
|
|
8
|
+
export interface DashboardStackProps extends NestedCloudProps {
|
|
9
|
+
dashboardName?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class DashboardStack {
|
|
13
|
+
// lambdaFunction: lambda.Function
|
|
14
|
+
dashboard: cloudwatch.Dashboard
|
|
15
|
+
|
|
16
|
+
constructor(scope: Construct, props: DashboardStackProps) {
|
|
17
|
+
const dashboardName = props.dashboardName || 'StacksDashboard'
|
|
18
|
+
|
|
19
|
+
// Create Sample Lambda Function which will create metrics
|
|
20
|
+
// this.lambdaFunction = new Function(this, 'SampleLambda', {
|
|
21
|
+
// handler: 'lambda-handler.handler',
|
|
22
|
+
// runtime: Runtime.PYTHON_3_7,
|
|
23
|
+
// code: new AssetCode(`./lambda`),
|
|
24
|
+
// memorySize: 512,
|
|
25
|
+
// timeout: Duration.seconds(10),
|
|
26
|
+
// })
|
|
27
|
+
|
|
28
|
+
// Create CloudWatch Dashboard
|
|
29
|
+
this.dashboard = new cloudwatch.Dashboard(scope, 'SampleLambdaDashboard', {
|
|
30
|
+
dashboardName,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Create Title for Dashboard
|
|
34
|
+
this.dashboard.addWidgets(new cloudwatch.TextWidget({
|
|
35
|
+
markdown: `# Dashboard: ${this.lambdaFunction.functionName}`,
|
|
36
|
+
height: 1,
|
|
37
|
+
width: 24,
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
// Create CloudWatch Dashboard Widgets: Errors, Invocations, Duration, Throttles
|
|
41
|
+
this.dashboard.addWidgets(new cloudwatch.GraphWidget({
|
|
42
|
+
title: 'Invocations',
|
|
43
|
+
left: [this.lambdaFunction.metricInvocations()],
|
|
44
|
+
width: 24,
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
this.dashboard.addWidgets(new cloudwatch.GraphWidget({
|
|
48
|
+
title: 'Errors',
|
|
49
|
+
left: [this.lambdaFunction.metricErrors()],
|
|
50
|
+
width: 24,
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
this.dashboard.addWidgets(new cloudwatch.GraphWidget({
|
|
54
|
+
title: 'Duration',
|
|
55
|
+
left: [this.lambdaFunction.metricDuration()],
|
|
56
|
+
width: 24,
|
|
57
|
+
}))
|
|
58
|
+
|
|
59
|
+
this.dashboard.addWidgets(new cloudwatch.GraphWidget({
|
|
60
|
+
title: 'Throttles',
|
|
61
|
+
left: [this.lambdaFunction.metricThrottles()],
|
|
62
|
+
width: 24,
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
// Create Widget to show last 20 Log Entries
|
|
66
|
+
this.dashboard.addWidgets(new cloudwatch.LogQueryWidget({
|
|
67
|
+
logGroupNames: [this.lambdaFunction.logGroup.logGroupName],
|
|
68
|
+
queryLines: [
|
|
69
|
+
'fields @timestamp, @message',
|
|
70
|
+
'sort @timestamp desc',
|
|
71
|
+
'limit 20',
|
|
72
|
+
],
|
|
73
|
+
width: 24,
|
|
74
|
+
}))
|
|
75
|
+
|
|
76
|
+
// Generate Output
|
|
77
|
+
const cloudwatchDashboardURL = `https://${Aws.REGION}.console.aws.amazon.com/cloudwatch/home?region=${Aws.REGION}#dashboards:name=${dashboardName}`
|
|
78
|
+
|
|
79
|
+
new Output(scope, 'DashboardOutput', {
|
|
80
|
+
value: cloudwatchDashboardURL,
|
|
81
|
+
description: 'The CloudWatch Dashboard URL',
|
|
82
|
+
exportName: 'StacksDashboardURL',
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* eslint-disable no-new */
|
|
2
|
+
import type { aws_cloudfront as cloudfront, aws_s3 as s3 } from 'aws-cdk-lib'
|
|
3
|
+
import { AssetHashType, aws_s3_deployment as s3deploy } from 'aws-cdk-lib'
|
|
4
|
+
import { config } from '@stacksjs/config'
|
|
5
|
+
import { websiteSourceHash } from '@stacksjs/utils'
|
|
6
|
+
import type { Construct } from 'constructs'
|
|
7
|
+
import type { NestedCloudProps } from '../types'
|
|
8
|
+
|
|
9
|
+
export interface DeploymentStackProps extends NestedCloudProps {
|
|
10
|
+
publicBucket: s3.Bucket
|
|
11
|
+
privateBucket: s3.Bucket
|
|
12
|
+
cdn: cloudfront.Distribution
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class DeploymentStack {
|
|
16
|
+
privateSource: string
|
|
17
|
+
docsSource: string
|
|
18
|
+
websiteSource: string
|
|
19
|
+
|
|
20
|
+
constructor(scope: Construct, props: DeploymentStackProps) {
|
|
21
|
+
// following paths are relative to where the command is run from
|
|
22
|
+
this.privateSource = '../../../private'
|
|
23
|
+
this.docsSource = '../../docs/dist/'
|
|
24
|
+
this.websiteSource = config.app.docMode === true ? this.docsSource : '../../views/dist/'
|
|
25
|
+
|
|
26
|
+
new s3deploy.BucketDeployment(scope, 'Website', {
|
|
27
|
+
sources: [s3deploy.Source.asset(this.websiteSource, {
|
|
28
|
+
assetHash: websiteSourceHash,
|
|
29
|
+
assetHashType: AssetHashType.CUSTOM,
|
|
30
|
+
})],
|
|
31
|
+
destinationBucket: props.publicBucket,
|
|
32
|
+
distribution: props.cdn,
|
|
33
|
+
distributionPaths: ['/*'],
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
new s3deploy.BucketDeployment(scope, 'PrivateFiles', {
|
|
37
|
+
sources: [s3deploy.Source.asset(this.privateSource)],
|
|
38
|
+
destinationBucket: props.privateBucket,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/cloud/dns.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* eslint-disable no-new */
|
|
2
|
+
import { RemovalPolicy, aws_route53 as route53, aws_s3 as s3, aws_route53_targets as targets } from 'aws-cdk-lib'
|
|
3
|
+
import type { Construct } from 'constructs'
|
|
4
|
+
import type { NestedCloudProps } from '../types'
|
|
5
|
+
|
|
6
|
+
export class DnsStack {
|
|
7
|
+
zone: route53.IHostedZone
|
|
8
|
+
|
|
9
|
+
constructor(scope: Construct, props: NestedCloudProps) {
|
|
10
|
+
// lets see if the zone already exists because Buddy should have created it already
|
|
11
|
+
this.zone = route53.PublicHostedZone.fromLookup(scope, 'AppUrlHostedZone', {
|
|
12
|
+
domainName: props.domain,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// setup the www redirect
|
|
16
|
+
// Create a bucket for www.yourdomain.com and configure it to redirect to yourdomain.com
|
|
17
|
+
const wwwBucket = new s3.Bucket(scope, 'WwwBucket', {
|
|
18
|
+
bucketName: `www.${props.domain}`,
|
|
19
|
+
websiteRedirect: {
|
|
20
|
+
hostName: props.domain,
|
|
21
|
+
protocol: s3.RedirectProtocol.HTTPS,
|
|
22
|
+
},
|
|
23
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
24
|
+
autoDeleteObjects: true,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Create a Route53 record for www.yourdomain.com
|
|
28
|
+
new route53.ARecord(scope, 'WwwAliasRecord', {
|
|
29
|
+
recordName: `www.${props.domain}`,
|
|
30
|
+
zone: this.zone,
|
|
31
|
+
target: route53.RecordTarget.fromAlias(new targets.BucketWebsiteTarget(wwwBucket)),
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* eslint-disable no-new */
|
|
2
|
+
import type { CfnResource } from 'aws-cdk-lib'
|
|
3
|
+
import { AssetHashType, CfnOutput as Output, RemovalPolicy, aws_lambda as lambda } from 'aws-cdk-lib'
|
|
4
|
+
import type { Construct } from 'constructs'
|
|
5
|
+
import { config } from '@stacksjs/config'
|
|
6
|
+
import { path as p } from '@stacksjs/path'
|
|
7
|
+
import { storage } from '@stacksjs/storage'
|
|
8
|
+
import { originRequestFunctionHash } from '@stacksjs/utils'
|
|
9
|
+
import type { NestedCloudProps } from '../types'
|
|
10
|
+
|
|
11
|
+
export interface DocsStackProps extends NestedCloudProps {
|
|
12
|
+
//
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class DocsStack {
|
|
16
|
+
originRequestFunction: lambda.Function
|
|
17
|
+
|
|
18
|
+
constructor(scope: Construct, props: DocsStackProps) {
|
|
19
|
+
// if docsPrefix is not set, then we know we are in docsMode and the documentation lives at the root of the domain
|
|
20
|
+
const docsPrefix = config.app.docMode ? '' : config.docs.base
|
|
21
|
+
|
|
22
|
+
// this edge function ensures pretty docs urls
|
|
23
|
+
// soon to be reused for our Meema features
|
|
24
|
+
this.originRequestFunction = new lambda.Function(scope, 'OriginRequestFunction', {
|
|
25
|
+
// this needs to have timestamp to ensure uniqueness. Since Origin Request (Lambda@Edge) functions are replicated functions, the
|
|
26
|
+
// deletion process takes a "long time". This way, the function is always unique in cases of quick recreations.
|
|
27
|
+
functionName: `${props.slug}-${props.appEnv}-origin-request-${props.timestamp}`,
|
|
28
|
+
description: 'The Stacks Origin Request function that prettifies URLs',
|
|
29
|
+
runtime: lambda.Runtime.NODEJS_18_X,
|
|
30
|
+
handler: 'dist/origin-request.handler',
|
|
31
|
+
code: lambda.Code.fromAsset(p.corePath('cloud/dist.zip'), {
|
|
32
|
+
assetHash: originRequestFunctionHash,
|
|
33
|
+
assetHashType: AssetHashType.CUSTOM,
|
|
34
|
+
}),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// applying this is a workaround for failing deployments due to the following DELETE_FAILED error:
|
|
38
|
+
// > Resource handler returned message: "Lambda was unable to delete arn:aws:lambda:us-east-1:92330274019:function:stacks-cloud-production-OriginRequestFunction4FA39-XQadJcSWY8Lz:1 because it is a replicated function. Please see our documentation for Deleting Lambda@Edge Functions and Replicas. (Service: Lambda, Status Code: 400, Request ID: 83bd3112-aaa4-4980-bfcf-3ee2052a0435)" (RequestToken: c91aed31-1a62-9425-c25d-4fc0fccfa45f, HandlerErrorCode: InvalidRequest)
|
|
39
|
+
// if we do not delete this resource, then it circumvents trying to delete the function and the deployment succeeds
|
|
40
|
+
// buddy cloud:cleanup is what will be suggested running after user ensured no more sensitive data is in the buckets
|
|
41
|
+
const cfnOriginRequestFunction = this.originRequestFunction.node.defaultChild as CfnResource
|
|
42
|
+
cfnOriginRequestFunction.applyRemovalPolicy(RemovalPolicy.RETAIN)
|
|
43
|
+
|
|
44
|
+
if (!config.app.docMode && storage.hasFiles(p.projectPath('docs'))) {
|
|
45
|
+
new Output(scope, 'DocsUrl', {
|
|
46
|
+
value: `https://${props.domain}/${docsPrefix}`,
|
|
47
|
+
description: 'The URL of the deployed documentation',
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/* eslint-disable no-new */
|
|
2
|
+
import { Duration, RemovalPolicy, Stack, Tags, aws_iam as iam, aws_lambda as lambda, aws_route53 as route53, aws_s3 as s3, aws_s3_notifications as s3n, aws_ses as ses } from 'aws-cdk-lib'
|
|
3
|
+
import { config } from '@stacksjs/config'
|
|
4
|
+
import type { Construct } from 'constructs'
|
|
5
|
+
import type { NestedCloudProps } from '../types'
|
|
6
|
+
|
|
7
|
+
export interface EmailStackProps extends NestedCloudProps {
|
|
8
|
+
zone: route53.IHostedZone
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class EmailStack {
|
|
12
|
+
emailBucket: s3.Bucket
|
|
13
|
+
|
|
14
|
+
constructor(scope: Construct, props: EmailStackProps) {
|
|
15
|
+
const bucketPrefix = `${props.slug}-${props.appEnv}`
|
|
16
|
+
this.emailBucket = new s3.Bucket(scope, 'EmailBucket', {
|
|
17
|
+
bucketName: `${bucketPrefix}-email-${props.timestamp}`,
|
|
18
|
+
versioned: true,
|
|
19
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
20
|
+
autoDeleteObjects: true,
|
|
21
|
+
// encryptionKey: this.encryptionKey,
|
|
22
|
+
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
23
|
+
lifecycleRules: [
|
|
24
|
+
{
|
|
25
|
+
id: '24h',
|
|
26
|
+
enabled: true,
|
|
27
|
+
expiration: Duration.days(1),
|
|
28
|
+
noncurrentVersionExpiration: Duration.days(1),
|
|
29
|
+
prefix: 'today/',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'Intelligent transition for Inbox',
|
|
33
|
+
enabled: true,
|
|
34
|
+
prefix: 'inbox/',
|
|
35
|
+
transitions: [
|
|
36
|
+
{
|
|
37
|
+
storageClass: s3.StorageClass.INTELLIGENT_TIERING,
|
|
38
|
+
transitionAfter: Duration.days(0),
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'Intelligent transition for Sent',
|
|
44
|
+
enabled: true,
|
|
45
|
+
prefix: 'sent/',
|
|
46
|
+
transitions: [
|
|
47
|
+
{
|
|
48
|
+
storageClass: s3.StorageClass.INTELLIGENT_TIERING,
|
|
49
|
+
transitionAfter: Duration.days(0),
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
Tags.of(this.emailBucket).add('daily-backup', 'true')
|
|
57
|
+
|
|
58
|
+
const sesPrincipal = new iam.ServicePrincipal('ses.amazonaws.com')
|
|
59
|
+
const ruleSetName = `${props.slug}-${props.appEnv}-email-receipt-rule-set`
|
|
60
|
+
const receiptRuleName = `${props.slug}-${props.appEnv}-email-receipt-rule`
|
|
61
|
+
|
|
62
|
+
const ruleSet = new ses.CfnReceiptRuleSet(scope, 'SESReceiptRuleSet', {
|
|
63
|
+
ruleSetName,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
this.emailBucket.addToResourcePolicy(
|
|
67
|
+
new iam.PolicyStatement({
|
|
68
|
+
sid: 'AllowSESPuts',
|
|
69
|
+
effect: iam.Effect.ALLOW,
|
|
70
|
+
principals: [sesPrincipal],
|
|
71
|
+
actions: [
|
|
72
|
+
's3:PutObject',
|
|
73
|
+
],
|
|
74
|
+
resources: [
|
|
75
|
+
`${this.emailBucket.bucketArn}/*`,
|
|
76
|
+
],
|
|
77
|
+
conditions: {
|
|
78
|
+
StringEquals: {
|
|
79
|
+
'aws:SourceAccount': Stack.of(scope).account,
|
|
80
|
+
},
|
|
81
|
+
ArnLike: {
|
|
82
|
+
'aws:SourceArn': `arn:aws:ses:${Stack.of(scope).region}:${Stack.of(scope).account}:receipt-rule-set/${ruleSetName}:receipt-rule/${receiptRuleName}`,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const receiptRule = new ses.CfnReceiptRule(scope, 'SESReceiptRule', {
|
|
89
|
+
ruleSetName: ruleSet.ref,
|
|
90
|
+
rule: {
|
|
91
|
+
name: receiptRuleName,
|
|
92
|
+
enabled: true,
|
|
93
|
+
actions: [
|
|
94
|
+
{
|
|
95
|
+
s3Action: {
|
|
96
|
+
bucketName: this.emailBucket.bucketName,
|
|
97
|
+
objectKeyPrefix: 'tmp/email_in/',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
recipients: config.email.mailboxes || [],
|
|
102
|
+
scanEnabled: config.email.server?.scan || true,
|
|
103
|
+
tlsPolicy: 'Require',
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// this line is important to make sure the bucket is created before the receipt rule
|
|
108
|
+
// do not remove it, unless you want a hell of a time debugging randomness
|
|
109
|
+
receiptRule.node.addDependency(this.emailBucket)
|
|
110
|
+
|
|
111
|
+
const iamGroup = new iam.Group(scope, 'IAMGroup', {
|
|
112
|
+
groupName: `${props.slug}-${props.appEnv}-email-management-s3-group`,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const listBucketsPolicyStatement = new iam.PolicyStatement({
|
|
116
|
+
effect: iam.Effect.ALLOW,
|
|
117
|
+
actions: ['s3:ListAllMyBuckets'],
|
|
118
|
+
resources: ['*'],
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const policyStatement = new iam.PolicyStatement({
|
|
122
|
+
effect: iam.Effect.ALLOW,
|
|
123
|
+
actions: [
|
|
124
|
+
's3:ListBucket',
|
|
125
|
+
's3:GetObject',
|
|
126
|
+
's3:PutObject',
|
|
127
|
+
's3:DeleteObject',
|
|
128
|
+
's3:GetObjectAcl',
|
|
129
|
+
's3:GetObjectVersionAcl',
|
|
130
|
+
's3:PutObjectAcl',
|
|
131
|
+
's3:PutObjectVersionAcl',
|
|
132
|
+
],
|
|
133
|
+
resources: [
|
|
134
|
+
this.emailBucket.bucketArn,
|
|
135
|
+
`${this.emailBucket.bucketArn}/*`,
|
|
136
|
+
],
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const policy = new iam.Policy(scope, 'EmailAccessPolicy', {
|
|
140
|
+
policyName: `${props.slug}-${props.appEnv}-email-management-s3-policy`,
|
|
141
|
+
statements: [policyStatement, listBucketsPolicyStatement],
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
iamGroup.attachInlinePolicy(policy)
|
|
145
|
+
|
|
146
|
+
// Create a SES domain identity
|
|
147
|
+
const sesIdentity = new ses.CfnEmailIdentity(scope, 'DomainIdentity', {
|
|
148
|
+
emailIdentity: props.domain,
|
|
149
|
+
|
|
150
|
+
dkimSigningAttributes: {
|
|
151
|
+
nextSigningKeyLength: 'RSA_2048_BIT',
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
dkimAttributes: {
|
|
155
|
+
signingEnabled: true,
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
mailFromAttributes: {
|
|
159
|
+
behaviorOnMxFailure: 'USE_DEFAULT_VALUE',
|
|
160
|
+
mailFromDomain: `mail.${props.domain}`,
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
feedbackAttributes: {
|
|
164
|
+
emailForwardingEnabled: true,
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Create a Route53 records for the SES domain identity
|
|
169
|
+
// https://github.com/aws/aws-cdk/issues/21306
|
|
170
|
+
new route53.CfnRecordSet(scope, 'DkimRecord1', {
|
|
171
|
+
hostedZoneName: `${props.zone.zoneName}.`,
|
|
172
|
+
name: sesIdentity.attrDkimDnsTokenName1,
|
|
173
|
+
type: 'CNAME',
|
|
174
|
+
resourceRecords: [sesIdentity.attrDkimDnsTokenValue1],
|
|
175
|
+
ttl: '1800',
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
new route53.CfnRecordSet(scope, 'DkimRecord2', {
|
|
179
|
+
hostedZoneName: `${props.zone.zoneName}.`,
|
|
180
|
+
name: sesIdentity.attrDkimDnsTokenName2,
|
|
181
|
+
type: 'CNAME',
|
|
182
|
+
resourceRecords: [sesIdentity.attrDkimDnsTokenValue2],
|
|
183
|
+
ttl: '1800',
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
new route53.CfnRecordSet(scope, 'DkimRecord3', {
|
|
187
|
+
hostedZoneName: `${props.zone.zoneName}.`,
|
|
188
|
+
name: sesIdentity.attrDkimDnsTokenName3,
|
|
189
|
+
type: 'CNAME',
|
|
190
|
+
resourceRecords: [sesIdentity.attrDkimDnsTokenValue3],
|
|
191
|
+
ttl: '1800',
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
new route53.MxRecord(scope, 'MxRecord', {
|
|
195
|
+
zone: props.zone,
|
|
196
|
+
recordName: 'mail',
|
|
197
|
+
values: [{
|
|
198
|
+
priority: 10,
|
|
199
|
+
hostName: 'feedback-smtp.us-east-1.amazonses.com',
|
|
200
|
+
}],
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
new route53.TxtRecord(scope, 'TxtSpfRecord', {
|
|
204
|
+
zone: props.zone,
|
|
205
|
+
recordName: 'mail',
|
|
206
|
+
values: ['v=spf1 include:amazonses.com ~all'],
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
new route53.TxtRecord(scope, 'TxtDmarcRecord', {
|
|
210
|
+
zone: props.zone,
|
|
211
|
+
recordName: '_dmarc',
|
|
212
|
+
values: [`v=DMARC1;p=quarantine;pct=25;rua=mailto:dmarcreports@${props.domain}`],
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const lambdaEmailOutboundRole = new iam.Role(scope, 'LambdaEmailOutboundRole', {
|
|
216
|
+
roleName: `${props.slug}-${props.appEnv}-email-outbound`,
|
|
217
|
+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
|
|
218
|
+
managedPolicies: [
|
|
219
|
+
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
|
|
220
|
+
],
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const lambdaEmailOutbound = new lambda.Function(scope, 'LambdaEmailOutbound', {
|
|
224
|
+
functionName: `${props.slug}-${props.appEnv}-email-outbound`,
|
|
225
|
+
description: 'Take the JSON and convert it in to an raw email.',
|
|
226
|
+
code: lambda.Code.fromInline('exports.handler = async (event) => {return true;};'), // this needs to be updated with the real lambda code
|
|
227
|
+
handler: 'index.handler',
|
|
228
|
+
memorySize: 256,
|
|
229
|
+
runtime: lambda.Runtime.NODEJS_18_X,
|
|
230
|
+
timeout: Duration.seconds(60),
|
|
231
|
+
environment: {
|
|
232
|
+
BUCKET: this.emailBucket.bucketName,
|
|
233
|
+
},
|
|
234
|
+
role: lambdaEmailOutboundRole,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
lambdaEmailOutboundRole.addToPolicy(policyStatement)
|
|
238
|
+
|
|
239
|
+
const sesPolicyStatement = new iam.PolicyStatement({
|
|
240
|
+
effect: iam.Effect.ALLOW,
|
|
241
|
+
actions: ['ses:SendRawEmail'],
|
|
242
|
+
resources: ['*'],
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
lambdaEmailOutboundRole.addToPolicy(sesPolicyStatement)
|
|
246
|
+
|
|
247
|
+
const lambdaEmailInboundRole = new iam.Role(scope, 'LambdaEmailInboundRole', {
|
|
248
|
+
roleName: `${props.slug}-${props.appEnv}-email-inbound`,
|
|
249
|
+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
|
|
250
|
+
managedPolicies: [
|
|
251
|
+
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
|
|
252
|
+
],
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const lambdaEmailInbound = new lambda.Function(scope, 'LambdaEmailInbound', {
|
|
256
|
+
functionName: `${props.slug}-${props.appEnv}-email-inbound`,
|
|
257
|
+
description: 'This Lambda organizes all the incoming emails based on the From and To field.',
|
|
258
|
+
code: lambda.Code.fromInline('exports.handler = async (event) => {return true;};'), // this needs to be updated with the real lambda code
|
|
259
|
+
handler: 'index.handler',
|
|
260
|
+
memorySize: 256,
|
|
261
|
+
role: lambdaEmailInboundRole,
|
|
262
|
+
runtime: lambda.Runtime.NODEJS_18_X,
|
|
263
|
+
timeout: Duration.seconds(60),
|
|
264
|
+
environment: {
|
|
265
|
+
BUCKET: this.emailBucket.bucketName,
|
|
266
|
+
},
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
new lambda.CfnPermission(scope, 'S3InboundPermission', {
|
|
270
|
+
action: 'lambda:InvokeFunction',
|
|
271
|
+
functionName: lambdaEmailInbound.functionName,
|
|
272
|
+
principal: 's3.amazonaws.com',
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
const inboundS3PolicyStatement = new iam.PolicyStatement({
|
|
276
|
+
effect: iam.Effect.ALLOW,
|
|
277
|
+
actions: ['s3:*'],
|
|
278
|
+
resources: [
|
|
279
|
+
this.emailBucket.bucketArn,
|
|
280
|
+
`${this.emailBucket.bucketArn}/*`,
|
|
281
|
+
],
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
lambdaEmailInboundRole.addToPolicy(inboundS3PolicyStatement)
|
|
285
|
+
|
|
286
|
+
const sesInboundPolicyStatement = new iam.PolicyStatement({
|
|
287
|
+
effect: iam.Effect.ALLOW,
|
|
288
|
+
actions: ['ses:ListIdentities'],
|
|
289
|
+
resources: ['*'],
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
lambdaEmailInboundRole.addToPolicy(sesInboundPolicyStatement)
|
|
293
|
+
|
|
294
|
+
const lambdaEmailConverterRole = new iam.Role(scope, 'LambdaEmailConverterRole', {
|
|
295
|
+
roleName: `${props.slug}-${props.appEnv}-email-converter`,
|
|
296
|
+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
|
|
297
|
+
managedPolicies: [
|
|
298
|
+
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
|
|
299
|
+
],
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
const lambdaEmailConverter = new lambda.Function(scope, 'LambdaEmailConverter', {
|
|
303
|
+
functionName: `${props.slug}-${props.appEnv}-email-converter`,
|
|
304
|
+
description: 'This Lambda converts raw emails files in to HTML and text.',
|
|
305
|
+
code: lambda.Code.fromInline('exports.handler = async (event) => {console.log("hello world email converter");return true;};'), // this needs to be updated with the real lambda code
|
|
306
|
+
handler: 'index.handler',
|
|
307
|
+
memorySize: 256,
|
|
308
|
+
role: lambdaEmailConverterRole,
|
|
309
|
+
runtime: lambda.Runtime.NODEJS_18_X,
|
|
310
|
+
timeout: Duration.seconds(60),
|
|
311
|
+
environment: {
|
|
312
|
+
BUCKET: this.emailBucket.bucketName,
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
const converterS3PolicyStatement = new iam.PolicyStatement({
|
|
317
|
+
effect: iam.Effect.ALLOW,
|
|
318
|
+
actions: ['s3:*'],
|
|
319
|
+
resources: [
|
|
320
|
+
this.emailBucket.bucketArn,
|
|
321
|
+
`${this.emailBucket.bucketArn}/*`,
|
|
322
|
+
],
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
new lambda.CfnPermission(scope, 'S3ConverterPermission', {
|
|
326
|
+
action: 'lambda:InvokeFunction',
|
|
327
|
+
functionName: lambdaEmailConverter.functionName,
|
|
328
|
+
principal: 's3.amazonaws.com',
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
lambdaEmailConverterRole.addToPolicy(converterS3PolicyStatement)
|
|
332
|
+
|
|
333
|
+
this.emailBucket.addEventNotification(s3.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(lambdaEmailInbound), { prefix: 'tmp/email_in/' })
|
|
334
|
+
this.emailBucket.addEventNotification(s3.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(lambdaEmailOutbound), { prefix: 'tmp/email_out/json/' })
|
|
335
|
+
this.emailBucket.addEventNotification(s3.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: 'sent/' })
|
|
336
|
+
this.emailBucket.addEventNotification(s3.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: 'inbox/' })
|
|
337
|
+
this.emailBucket.addEventNotification(s3.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: 'today/' })
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { aws_ec2 as ec2 } from 'aws-cdk-lib'
|
|
2
|
+
import { RemovalPolicy, aws_efs as efs } from 'aws-cdk-lib'
|
|
3
|
+
import type { Construct } from 'constructs'
|
|
4
|
+
import type { NestedCloudProps } from '../types'
|
|
5
|
+
|
|
6
|
+
export interface FileSystemStackProps extends NestedCloudProps {
|
|
7
|
+
vpc: ec2.Vpc
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class FileSystemStack {
|
|
11
|
+
fileSystem: efs.FileSystem
|
|
12
|
+
accessPoint: efs.AccessPoint
|
|
13
|
+
|
|
14
|
+
constructor(scope: Construct, props: FileSystemStackProps) {
|
|
15
|
+
this.fileSystem = new efs.FileSystem(scope, 'FileSystem', {
|
|
16
|
+
fileSystemName: `${props.slug}-${props.appEnv}-efs`,
|
|
17
|
+
vpc: props.vpc,
|
|
18
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
19
|
+
lifecyclePolicy: efs.LifecyclePolicy.AFTER_7_DAYS,
|
|
20
|
+
performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
|
|
21
|
+
throughputMode: efs.ThroughputMode.BURSTING,
|
|
22
|
+
enableAutomaticBackups: true, // TODO: ensure this is documented
|
|
23
|
+
encrypted: true,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
this.accessPoint = new efs.AccessPoint(scope, 'FileSystemAccessPoint', {
|
|
27
|
+
fileSystem: this.fileSystem,
|
|
28
|
+
path: '/',
|
|
29
|
+
posixUser: {
|
|
30
|
+
uid: '1000',
|
|
31
|
+
gid: '1000',
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/* eslint-disable no-new */
|
|
2
|
+
import { Stack } from 'aws-cdk-lib'
|
|
3
|
+
import type { Construct } from 'constructs'
|
|
4
|
+
import { config } from '@stacksjs/config'
|
|
5
|
+
import type { CloudOptions } from '../types'
|
|
6
|
+
import { AiStack } from './ai'
|
|
7
|
+
import { CdnStack } from './cdn'
|
|
8
|
+
import { CliStack } from './cli'
|
|
9
|
+
import { DnsStack } from './dns'
|
|
10
|
+
import { DocsStack } from './docs'
|
|
11
|
+
import { StorageStack } from './storage'
|
|
12
|
+
import { SecurityStack } from './security'
|
|
13
|
+
import { DeploymentStack } from './deployment'
|
|
14
|
+
import { JumpBoxStack } from './jump-box'
|
|
15
|
+
import { FileSystemStack } from './file-system'
|
|
16
|
+
import { NetworkStack } from './network'
|
|
17
|
+
import { RedirectsStack } from './redirects'
|
|
18
|
+
import { EmailStack } from './email'
|
|
19
|
+
import { PermissionsStack } from './permissions'
|
|
20
|
+
import { ComputeStack } from './compute'
|
|
21
|
+
|
|
22
|
+
// import { DashboardStack } from './dashboard'
|
|
23
|
+
|
|
24
|
+
export class Cloud extends Stack {
|
|
25
|
+
constructor(scope: Construct, id: string, props: CloudOptions) {
|
|
26
|
+
super(scope, id, props)
|
|
27
|
+
// please beware: be careful changing the order of the stack creations below
|
|
28
|
+
const dns = new DnsStack(this, props)
|
|
29
|
+
|
|
30
|
+
const security = new SecurityStack(this, {
|
|
31
|
+
...props,
|
|
32
|
+
zone: dns.zone,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const storage = new StorageStack(this, {
|
|
36
|
+
...props,
|
|
37
|
+
kmsKey: security.kmsKey,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const network = new NetworkStack(this, props)
|
|
41
|
+
|
|
42
|
+
const fileSystem = new FileSystemStack(this, {
|
|
43
|
+
...props,
|
|
44
|
+
vpc: network.vpc,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
new JumpBoxStack(this, {
|
|
48
|
+
...props,
|
|
49
|
+
vpc: network.vpc,
|
|
50
|
+
fileSystem: fileSystem.fileSystem,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const docs = new DocsStack(this, props)
|
|
54
|
+
|
|
55
|
+
new EmailStack(this, {
|
|
56
|
+
...props,
|
|
57
|
+
zone: dns.zone,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
new RedirectsStack(this, props)
|
|
61
|
+
|
|
62
|
+
new PermissionsStack(this)
|
|
63
|
+
|
|
64
|
+
// new DashboardStack(this)
|
|
65
|
+
|
|
66
|
+
let api
|
|
67
|
+
if (config.cloud.api?.deploy) {
|
|
68
|
+
api = new ComputeStack(this, {
|
|
69
|
+
...props,
|
|
70
|
+
vpc: network.vpc,
|
|
71
|
+
fileSystem: fileSystem.fileSystem,
|
|
72
|
+
zone: dns.zone,
|
|
73
|
+
certificate: security.certificate,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ai = new AiStack(this, props)
|
|
78
|
+
|
|
79
|
+
const cli = new CliStack(this, props)
|
|
80
|
+
|
|
81
|
+
const cdn = new CdnStack(this, {
|
|
82
|
+
...props,
|
|
83
|
+
publicBucket: storage.publicBucket,
|
|
84
|
+
logBucket: storage.logBucket,
|
|
85
|
+
certificate: security.certificate,
|
|
86
|
+
firewall: security.firewall,
|
|
87
|
+
originRequestFunction: docs.originRequestFunction,
|
|
88
|
+
zone: dns.zone,
|
|
89
|
+
cliSetupUrl: cli.cliSetupUrl,
|
|
90
|
+
askAiUrl: ai.askAiUrl,
|
|
91
|
+
summarizeAiUrl: ai.summarizeAiUrl,
|
|
92
|
+
webServer: api?.apiServer,
|
|
93
|
+
webServerUrl: api?.apiServerUrl,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
new DeploymentStack(this, {
|
|
97
|
+
...props,
|
|
98
|
+
publicBucket: storage.publicBucket,
|
|
99
|
+
privateBucket: storage.privateBucket,
|
|
100
|
+
cdn: cdn.distribution,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|