@stacksjs/cloud 0.58.48 → 0.58.49
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/package.json +3 -2
- 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 +80 -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 +99 -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 +52 -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/stacksjs-router-0.58.48.tgz +0 -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
package/src/cloud/cdn.ts
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/* eslint-disable no-new */
|
|
2
|
+
import type { aws_certificatemanager as acm, aws_lambda as lambda, aws_s3 as s3, aws_wafv2 as wafv2 } from 'aws-cdk-lib'
|
|
3
|
+
import { Duration, Fn, CfnOutput as Output, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_route53 as route53, aws_route53_targets as targets } from 'aws-cdk-lib'
|
|
4
|
+
import type { Construct } from 'constructs'
|
|
5
|
+
import { config } from '@stacksjs/config'
|
|
6
|
+
import { hasFiles } from '@stacksjs/storage'
|
|
7
|
+
import { path as p } from '@stacksjs/path'
|
|
8
|
+
import { env } from '@stacksjs/env'
|
|
9
|
+
import type { NestedCloudProps } from '../types'
|
|
10
|
+
import type { EnvKey } from '../../../../env'
|
|
11
|
+
|
|
12
|
+
export interface CdnStackProps extends NestedCloudProps {
|
|
13
|
+
certificate: acm.Certificate
|
|
14
|
+
logBucket: s3.Bucket
|
|
15
|
+
publicBucket: s3.Bucket
|
|
16
|
+
firewall: wafv2.CfnWebACL
|
|
17
|
+
originRequestFunction: lambda.Function
|
|
18
|
+
zone: route53.IHostedZone
|
|
19
|
+
webServer: lambda.Function
|
|
20
|
+
webServerUrl: lambda.FunctionUrl
|
|
21
|
+
cliSetupUrl: lambda.FunctionUrl
|
|
22
|
+
askAiUrl: lambda.FunctionUrl
|
|
23
|
+
summarizeAiUrl: lambda.FunctionUrl
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class CdnStack {
|
|
27
|
+
distribution: cloudfront.Distribution
|
|
28
|
+
originAccessIdentity: cloudfront.OriginAccessIdentity
|
|
29
|
+
cdnCachePolicy: cloudfront.CachePolicy
|
|
30
|
+
apiCachePolicy: cloudfront.CachePolicy | undefined
|
|
31
|
+
vanityUrl: string
|
|
32
|
+
props: CdnStackProps
|
|
33
|
+
|
|
34
|
+
constructor(scope: Construct, props: CdnStackProps) {
|
|
35
|
+
this.props = props
|
|
36
|
+
|
|
37
|
+
this.originAccessIdentity = new cloudfront.OriginAccessIdentity(scope, 'OAI')
|
|
38
|
+
|
|
39
|
+
this.cdnCachePolicy = new cloudfront.CachePolicy(scope, 'CdnCachePolicy', {
|
|
40
|
+
comment: 'Stacks CDN Cache Policy',
|
|
41
|
+
cachePolicyName: `${props.slug}-${props.appEnv}-cdn-cache-policy`,
|
|
42
|
+
minTtl: config.cloud.cdn?.minTtl ? Duration.seconds(config.cloud.cdn.minTtl) : undefined,
|
|
43
|
+
defaultTtl: config.cloud.cdn?.defaultTtl ? Duration.seconds(config.cloud.cdn.defaultTtl) : undefined,
|
|
44
|
+
maxTtl: config.cloud.cdn?.maxTtl ? Duration.seconds(config.cloud.cdn.maxTtl) : undefined,
|
|
45
|
+
cookieBehavior: this.getCookieBehavior(config.cloud.cdn?.cookieBehavior),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// the actual CDN distribution
|
|
49
|
+
this.distribution = new cloudfront.Distribution(scope, 'Cdn', {
|
|
50
|
+
domainNames: [props.domain],
|
|
51
|
+
defaultRootObject: 'index.html',
|
|
52
|
+
comment: `CDN for ${config.app.url}`,
|
|
53
|
+
certificate: props.certificate,
|
|
54
|
+
enableLogging: true,
|
|
55
|
+
logBucket: props.logBucket,
|
|
56
|
+
httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
|
|
57
|
+
priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
|
|
58
|
+
enabled: true,
|
|
59
|
+
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
|
|
60
|
+
webAclId: props.firewall.attrArn,
|
|
61
|
+
enableIpv6: true,
|
|
62
|
+
|
|
63
|
+
defaultBehavior: {
|
|
64
|
+
origin: new origins.S3Origin(props.publicBucket, {
|
|
65
|
+
originAccessIdentity: this.originAccessIdentity,
|
|
66
|
+
}),
|
|
67
|
+
edgeLambdas: [
|
|
68
|
+
{
|
|
69
|
+
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
|
|
70
|
+
functionVersion: props.originRequestFunction.currentVersion,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
compress: config.cloud.cdn?.compress,
|
|
74
|
+
allowedMethods: this.allowedMethods(),
|
|
75
|
+
cachedMethods: this.cachedMethods(),
|
|
76
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
77
|
+
cachePolicy: this.cdnCachePolicy,
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
additionalBehaviors: this.additionalBehaviors(scope, props),
|
|
81
|
+
|
|
82
|
+
// Add custom error responses
|
|
83
|
+
errorResponses: [
|
|
84
|
+
{
|
|
85
|
+
httpStatus: 403,
|
|
86
|
+
responsePagePath: '/index.html',
|
|
87
|
+
responseHttpStatus: 200,
|
|
88
|
+
ttl: Duration.seconds(0),
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
new route53.ARecord(scope, 'AliasRecord', {
|
|
94
|
+
recordName: props.domain,
|
|
95
|
+
zone: props.zone,
|
|
96
|
+
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(this.distribution)),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
new Output(scope, 'DistributionId', {
|
|
100
|
+
value: this.distribution.distributionId,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
new Output(scope, 'AppUrl', {
|
|
104
|
+
value: `https://${props.domain}`,
|
|
105
|
+
description: 'The URL of the deployed application',
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
this.vanityUrl = `https://${this.distribution.domainName}`
|
|
109
|
+
new Output(scope, 'AppVanityUrl', {
|
|
110
|
+
value: this.vanityUrl,
|
|
111
|
+
description: 'The vanity URL of the deployed application',
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// this.originAccessIdentity = originAccessIdentity
|
|
115
|
+
// this.cdnCachePolicy = cdnCachePolicy
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getCookieBehavior(behavior: string | undefined): cloudfront.CacheCookieBehavior | undefined {
|
|
119
|
+
switch (behavior) {
|
|
120
|
+
case 'all':
|
|
121
|
+
return cloudfront.CacheCookieBehavior.all()
|
|
122
|
+
case 'none':
|
|
123
|
+
return cloudfront.CacheCookieBehavior.none()
|
|
124
|
+
case 'allowList':
|
|
125
|
+
// If you have a list of cookies, replace `myCookie` with your cookie
|
|
126
|
+
return cloudfront.CacheCookieBehavior.allowList(...config.cloud.cdn?.allowList.cookies || [])
|
|
127
|
+
default:
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
allowedMethods(): cloudfront.AllowedMethods {
|
|
133
|
+
switch (config.cloud.cdn?.allowedMethods) {
|
|
134
|
+
case 'ALL':
|
|
135
|
+
return cloudfront.AllowedMethods.ALLOW_ALL
|
|
136
|
+
case 'GET_HEAD':
|
|
137
|
+
return cloudfront.AllowedMethods.ALLOW_GET_HEAD
|
|
138
|
+
case 'GET_HEAD_OPTIONS':
|
|
139
|
+
return cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS
|
|
140
|
+
default:
|
|
141
|
+
return cloudfront.AllowedMethods.ALLOW_ALL
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
cachedMethods(): cloudfront.CachedMethods {
|
|
146
|
+
switch (config.cloud.cdn?.cachedMethods) {
|
|
147
|
+
case 'GET_HEAD':
|
|
148
|
+
return cloudfront.CachedMethods.CACHE_GET_HEAD
|
|
149
|
+
case 'GET_HEAD_OPTIONS':
|
|
150
|
+
return cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS
|
|
151
|
+
default:
|
|
152
|
+
return cloudfront.CachedMethods.CACHE_GET_HEAD
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
allowedMethodsFromString(methods?: 'ALL' | 'GET_HEAD' | 'GET_HEAD_OPTIONS'): cloudfront.AllowedMethods {
|
|
157
|
+
if (!methods)
|
|
158
|
+
return cloudfront.AllowedMethods.ALLOW_ALL
|
|
159
|
+
|
|
160
|
+
switch (methods) {
|
|
161
|
+
case 'ALL':
|
|
162
|
+
return cloudfront.AllowedMethods.ALLOW_ALL
|
|
163
|
+
case 'GET_HEAD':
|
|
164
|
+
return cloudfront.AllowedMethods.ALLOW_GET_HEAD
|
|
165
|
+
case 'GET_HEAD_OPTIONS':
|
|
166
|
+
return cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS
|
|
167
|
+
default:
|
|
168
|
+
return cloudfront.AllowedMethods.ALLOW_ALL
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
cachedMethodsFromString(methods?: 'GET_HEAD' | 'GET_HEAD_OPTIONS'): cloudfront.CachedMethods {
|
|
173
|
+
if (!methods)
|
|
174
|
+
return cloudfront.CachedMethods.CACHE_GET_HEAD
|
|
175
|
+
|
|
176
|
+
switch (methods) {
|
|
177
|
+
case 'GET_HEAD':
|
|
178
|
+
return cloudfront.CachedMethods.CACHE_GET_HEAD
|
|
179
|
+
case 'GET_HEAD_OPTIONS':
|
|
180
|
+
return cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS
|
|
181
|
+
default:
|
|
182
|
+
return cloudfront.CachedMethods.CACHE_GET_HEAD
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
shouldDeployApi() {
|
|
187
|
+
return config.cloud.api
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
apiBehaviorOptions(scope: Construct, props: CdnStackProps): Record<string, cloudfront.BehaviorOptions> {
|
|
191
|
+
const hostname = Fn.select(2, Fn.split('/', props.webServerUrl.url))
|
|
192
|
+
const origin = (path: '/api' | '/api/*' = '/api') => {
|
|
193
|
+
return new origins.HttpOrigin(hostname, {
|
|
194
|
+
originPath: path,
|
|
195
|
+
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
'/api': {
|
|
201
|
+
origin: origin(),
|
|
202
|
+
compress: true,
|
|
203
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
|
|
204
|
+
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
|
|
205
|
+
cachePolicy: this.setApiCachePolicy(scope),
|
|
206
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
207
|
+
},
|
|
208
|
+
'/api/*': {
|
|
209
|
+
origin: origin('/api/*'),
|
|
210
|
+
compress: true,
|
|
211
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
|
|
212
|
+
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
|
|
213
|
+
cachePolicy: this.apiCachePolicy,
|
|
214
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
docsBehaviorOptions(props: CdnStackProps): Record<string, cloudfront.BehaviorOptions> {
|
|
220
|
+
return {
|
|
221
|
+
'/docs': {
|
|
222
|
+
origin: new origins.S3Origin(props.publicBucket, {
|
|
223
|
+
originAccessIdentity: this.originAccessIdentity,
|
|
224
|
+
originPath: '/docs',
|
|
225
|
+
}),
|
|
226
|
+
compress: true,
|
|
227
|
+
allowedMethods: this.allowedMethodsFromString(config.cloud.cdn?.allowedMethods),
|
|
228
|
+
cachedMethods: this.cachedMethodsFromString(config.cloud.cdn?.cachedMethods),
|
|
229
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
230
|
+
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
|
|
231
|
+
},
|
|
232
|
+
'/docs/*': {
|
|
233
|
+
origin: new origins.S3Origin(props.publicBucket, {
|
|
234
|
+
originAccessIdentity: this.originAccessIdentity,
|
|
235
|
+
originPath: '/docs',
|
|
236
|
+
}),
|
|
237
|
+
compress: true,
|
|
238
|
+
allowedMethods: this.allowedMethodsFromString(config.cloud.cdn?.allowedMethods),
|
|
239
|
+
cachedMethods: this.cachedMethodsFromString(config.cloud.cdn?.cachedMethods),
|
|
240
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
241
|
+
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
aiBehaviorOptions(scope: Construct, props: CdnStackProps): Record<string, cloudfront.BehaviorOptions> {
|
|
247
|
+
const hostname = Fn.select(2, Fn.split('/', props.askAiUrl.url))
|
|
248
|
+
const summaryHostname = Fn.select(2, Fn.split('/', props.summarizeAiUrl.url))
|
|
249
|
+
|
|
250
|
+
const aiCachePolicy = new cloudfront.CachePolicy(scope, 'AiCachePolicy', {
|
|
251
|
+
comment: 'Stacks AI Cache Policy',
|
|
252
|
+
cachePolicyName: `${this.props.slug}-${this.props.appEnv}-ai-cache-policy`,
|
|
253
|
+
defaultTtl: Duration.seconds(0),
|
|
254
|
+
// minTtl: config.cloud.cdn?.minTtl ? Duration.seconds(config.cloud.cdn.minTtl) : undefined,
|
|
255
|
+
cookieBehavior: cloudfront.CacheCookieBehavior.none(),
|
|
256
|
+
headerBehavior: cloudfront.CacheHeaderBehavior.allowList('Accept', 'x-api-key', 'Authorization', 'Content-Type'),
|
|
257
|
+
queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(),
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
'/ai/ask': {
|
|
262
|
+
origin: new origins.HttpOrigin(hostname, {
|
|
263
|
+
originPath: '/ai',
|
|
264
|
+
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
|
|
265
|
+
}),
|
|
266
|
+
compress: false,
|
|
267
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
|
|
268
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
269
|
+
cachePolicy: aiCachePolicy,
|
|
270
|
+
},
|
|
271
|
+
'/ai/summary': {
|
|
272
|
+
origin: new origins.HttpOrigin(summaryHostname, {
|
|
273
|
+
originPath: '/ai',
|
|
274
|
+
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
|
|
275
|
+
}),
|
|
276
|
+
compress: false,
|
|
277
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
|
|
278
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
279
|
+
cachePolicy: aiCachePolicy,
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
cliSetupBehaviorOptions(scope: Construct, props: CdnStackProps): Record<string, cloudfront.BehaviorOptions> {
|
|
285
|
+
const hostname = Fn.select(2, Fn.split('/', props.cliSetupUrl.url))
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
'/install': {
|
|
289
|
+
origin: new origins.HttpOrigin(hostname, {
|
|
290
|
+
// origin: new origins.HttpOrigin('tipevv3dfx35fb7ptyq7nrxtga0qkcgc.lambda-url.us-east-1.on.aws', {
|
|
291
|
+
originPath: '/cli-setup',
|
|
292
|
+
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
|
|
293
|
+
}),
|
|
294
|
+
compress: false,
|
|
295
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
|
|
296
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
297
|
+
cachePolicy: new cloudfront.CachePolicy(scope, 'CliSetupCachePolicy', {
|
|
298
|
+
comment: 'Stacks CLI Setup Cache Policy',
|
|
299
|
+
cachePolicyName: `${this.props.slug}-${this.props.appEnv}-cli-setup-cache-policy`,
|
|
300
|
+
defaultTtl: Duration.seconds(0),
|
|
301
|
+
// minTtl: config.cloud.cdn?.minTtl ? Duration.seconds(config.cloud.cdn.minTtl) : undefined,
|
|
302
|
+
cookieBehavior: cloudfront.CacheCookieBehavior.none(),
|
|
303
|
+
headerBehavior: cloudfront.CacheHeaderBehavior.none(),
|
|
304
|
+
queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
|
|
305
|
+
}),
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
shouldDeployAiEndpoints() {
|
|
311
|
+
return config.cloud.ai
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
shouldDeployCliSetup() {
|
|
315
|
+
return config.cloud.cli
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
shouldDeployDocs() {
|
|
319
|
+
return hasFiles(p.projectPath('docs'))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
additionalBehaviors(scope: Construct, props: CdnStackProps): Record<string, cloudfront.BehaviorOptions> {
|
|
323
|
+
let behaviorOptions: Record<string, cloudfront.BehaviorOptions> = {}
|
|
324
|
+
|
|
325
|
+
if (this.shouldDeployApi()) {
|
|
326
|
+
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', '_']
|
|
327
|
+
keysToRemove.forEach(key => delete env[key as EnvKey])
|
|
328
|
+
|
|
329
|
+
behaviorOptions = this.apiBehaviorOptions(scope, props)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// if docMode is used, we don't need to add a behavior for the docs
|
|
333
|
+
// because the docs will be the root of the site
|
|
334
|
+
if (this.shouldDeployDocs() && !config.app.docMode) {
|
|
335
|
+
behaviorOptions = {
|
|
336
|
+
...this.docsBehaviorOptions(props),
|
|
337
|
+
...behaviorOptions,
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (this.shouldDeployAiEndpoints()) {
|
|
342
|
+
behaviorOptions = {
|
|
343
|
+
...this.aiBehaviorOptions(scope, props),
|
|
344
|
+
...behaviorOptions,
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (this.shouldDeployCliSetup()) {
|
|
349
|
+
behaviorOptions = {
|
|
350
|
+
...this.cliSetupBehaviorOptions(scope, props),
|
|
351
|
+
...behaviorOptions,
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return behaviorOptions
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
setApiCachePolicy(scope: Construct) {
|
|
359
|
+
if (this.apiCachePolicy)
|
|
360
|
+
return this.apiCachePolicy
|
|
361
|
+
|
|
362
|
+
this.apiCachePolicy = new cloudfront.CachePolicy(scope, 'ApiCachePolicy', {
|
|
363
|
+
comment: 'Stacks API Cache Policy',
|
|
364
|
+
cachePolicyName: `${this.props.slug}-${this.props.appEnv}-api-cache-policy`,
|
|
365
|
+
defaultTtl: Duration.seconds(0),
|
|
366
|
+
// minTtl: config.cloud.cdn?.minTtl ? Duration.seconds(config.cloud.cdn.minTtl) : undefined,
|
|
367
|
+
cookieBehavior: cloudfront.CacheCookieBehavior.none(),
|
|
368
|
+
headerBehavior: cloudfront.CacheHeaderBehavior.allowList('Accept', 'x-api-key', 'Authorization', 'Content-Type'),
|
|
369
|
+
queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
return this.apiCachePolicy
|
|
373
|
+
}
|
|
374
|
+
}
|
package/src/cloud/cli.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* eslint-disable no-new */
|
|
2
|
+
import { Duration, CfnOutput as Output, aws_lambda as lambda } from 'aws-cdk-lib'
|
|
3
|
+
import type { Construct } from 'constructs'
|
|
4
|
+
import type { NestedCloudProps } from '../types'
|
|
5
|
+
|
|
6
|
+
export interface CliStackProps extends NestedCloudProps {
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class CliStack {
|
|
10
|
+
cliSetupUrl: lambda.FunctionUrl
|
|
11
|
+
|
|
12
|
+
constructor(scope: Construct, props: CliStackProps) {
|
|
13
|
+
const cliSetupFunc = new lambda.Function(scope, 'CliSetupFunction', {
|
|
14
|
+
functionName: `${props.slug}-${props.appEnv}-cli-setup`,
|
|
15
|
+
description: 'Lambda function that triggers setup script for a Stacks project',
|
|
16
|
+
runtime: lambda.Runtime.NODEJS_20_X,
|
|
17
|
+
handler: 'index.handler',
|
|
18
|
+
code: lambda.Code.fromAsset('src/cloud/lambda/cli-setup'), // path relative to the cloud root package dir
|
|
19
|
+
timeout: Duration.seconds(30),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// create a Lambda.FunctionUrl to be used in the CloudFront OriginRequestPolicy
|
|
23
|
+
// this will be used to trigger the function
|
|
24
|
+
this.cliSetupUrl = new lambda.FunctionUrl(scope, 'CliSetupFunctionUrl', {
|
|
25
|
+
function: cliSetupFunc,
|
|
26
|
+
authType: lambda.FunctionUrlAuthType.NONE,
|
|
27
|
+
cors: {
|
|
28
|
+
allowedOrigins: ['*'],
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
new Output(scope, 'CliSetupVanityUrl', {
|
|
33
|
+
value: `${this.cliSetupUrl.url}cli-setup`,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
new Output(scope, 'CliSetupUrl', {
|
|
37
|
+
value: `https://${props.domain}/install`,
|
|
38
|
+
description: 'URL to trigger the CLI setup function',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// once deployed, need to create logic in the cdn origin request to check if the request is for the cli
|
|
42
|
+
// if it is, then we need the to use the function url as the origin
|
|
43
|
+
// if it is not, then we need don't adjust the origin
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* eslint-disable no-new */
|
|
2
|
+
import type { aws_certificatemanager as acm, aws_ec2 as ec2, aws_efs as efs, aws_route53 as route53 } from 'aws-cdk-lib'
|
|
3
|
+
import { Duration, CfnOutput as Output, aws_lambda as lambda, aws_logs as logs, aws_secretsmanager as secretsmanager } from 'aws-cdk-lib'
|
|
4
|
+
import type { Construct } from 'constructs'
|
|
5
|
+
import { path as p } from '@stacksjs/path'
|
|
6
|
+
import { env } from '@stacksjs/env'
|
|
7
|
+
import type { NestedCloudProps } from '../types'
|
|
8
|
+
import type { EnvKey } from '../../../../env'
|
|
9
|
+
|
|
10
|
+
export interface ComputeStackProps extends NestedCloudProps {
|
|
11
|
+
vpc: ec2.Vpc
|
|
12
|
+
fileSystem: efs.FileSystem
|
|
13
|
+
zone: route53.IHostedZone
|
|
14
|
+
certificate: acm.Certificate
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ComputeStack {
|
|
18
|
+
apiServer: lambda.Function
|
|
19
|
+
apiServerUrl: lambda.FunctionUrl
|
|
20
|
+
|
|
21
|
+
constructor(scope: Construct, props: ComputeStackProps) {
|
|
22
|
+
const vpc = props.vpc
|
|
23
|
+
const fileSystem = props.fileSystem
|
|
24
|
+
|
|
25
|
+
if (!fileSystem)
|
|
26
|
+
throw new Error('The file system is missing. Please make sure it was created properly.')
|
|
27
|
+
|
|
28
|
+
// const dockerImageAsset = new ecr_assets.DockerImageAsset(scope, 'ServerBuildImage', {
|
|
29
|
+
// directory: p.cloudPath('src/server'),
|
|
30
|
+
// })
|
|
31
|
+
|
|
32
|
+
this.apiServer = new lambda.Function(scope, 'WebServer', {
|
|
33
|
+
functionName: `${props.slug}-${props.appEnv}-web-server`,
|
|
34
|
+
description: 'The web server for the Stacks application',
|
|
35
|
+
code: lambda.Code.fromAssetImage(p.frameworkPath('server')),
|
|
36
|
+
handler: lambda.Handler.FROM_IMAGE,
|
|
37
|
+
runtime: lambda.Runtime.FROM_IMAGE,
|
|
38
|
+
vpc,
|
|
39
|
+
memorySize: 512, // replace with your actual memory size
|
|
40
|
+
timeout: Duration.minutes(5), // replace with your actual timeout
|
|
41
|
+
logRetention: logs.RetentionDays.ONE_WEEK,
|
|
42
|
+
architecture: lambda.Architecture.ARM_64,
|
|
43
|
+
// filesystem: lambda.FileSystem.fromEfsAccessPoint(props.accessPoint, '/mnt/efs'),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
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', '_']
|
|
47
|
+
keysToRemove.forEach(key => delete env[key as EnvKey])
|
|
48
|
+
|
|
49
|
+
const secrets = new secretsmanager.Secret(scope, 'StacksSecrets', {
|
|
50
|
+
secretName: `${props.slug}-${props.appEnv}-secrets`,
|
|
51
|
+
description: 'Secrets for the Stacks application',
|
|
52
|
+
generateSecretString: {
|
|
53
|
+
secretStringTemplate: JSON.stringify(env),
|
|
54
|
+
generateStringKey: Object.keys(env).join(',').length.toString(),
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
secrets.grantRead(this.apiServer)
|
|
59
|
+
this.apiServer.addEnvironment('SECRETS_ARN', secrets.secretArn)
|
|
60
|
+
|
|
61
|
+
this.apiServerUrl = new lambda.FunctionUrl(scope, 'StacksServerUrl', {
|
|
62
|
+
function: this.apiServer,
|
|
63
|
+
authType: lambda.FunctionUrlAuthType.NONE, // becomes a public API
|
|
64
|
+
cors: {
|
|
65
|
+
allowedOrigins: ['*'],
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const apiPrefix = 'api'
|
|
70
|
+
new Output(scope, 'ApiUrl', {
|
|
71
|
+
value: `https://${props.domain}/${apiPrefix}`,
|
|
72
|
+
description: 'The URL of the deployed application',
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
new Output(scope, 'ApiVanityUrl', {
|
|
76
|
+
value: this.apiServerUrl.url,
|
|
77
|
+
description: 'The Vanity URL of the deployed application',
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -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
|
+
}
|