@stacksjs/ts-cloud-core 0.1.1
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/LICENSE.md +21 -0
- package/README.md +321 -0
- package/package.json +31 -0
- package/src/advanced-features.test.ts +465 -0
- package/src/aws/cloudformation.ts +421 -0
- package/src/aws/cloudfront.ts +158 -0
- package/src/aws/credentials.test.ts +132 -0
- package/src/aws/credentials.ts +545 -0
- package/src/aws/index.ts +87 -0
- package/src/aws/s3.test.ts +188 -0
- package/src/aws/s3.ts +1088 -0
- package/src/aws/signature.test.ts +670 -0
- package/src/aws/signature.ts +1155 -0
- package/src/backup/disaster-recovery.test.ts +726 -0
- package/src/backup/disaster-recovery.ts +500 -0
- package/src/backup/index.ts +34 -0
- package/src/backup/manager.test.ts +498 -0
- package/src/backup/manager.ts +432 -0
- package/src/cicd/circleci.ts +430 -0
- package/src/cicd/github-actions.ts +424 -0
- package/src/cicd/gitlab-ci.ts +255 -0
- package/src/cicd/index.ts +8 -0
- package/src/cli/history.ts +396 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/progress.ts +458 -0
- package/src/cli/repl.ts +454 -0
- package/src/cli/suggestions.ts +327 -0
- package/src/cli/table.test.ts +319 -0
- package/src/cli/table.ts +332 -0
- package/src/cloudformation/builder.test.ts +327 -0
- package/src/cloudformation/builder.ts +378 -0
- package/src/cloudformation/builders/api-gateway.ts +449 -0
- package/src/cloudformation/builders/cache.ts +334 -0
- package/src/cloudformation/builders/cdn.ts +278 -0
- package/src/cloudformation/builders/compute.ts +485 -0
- package/src/cloudformation/builders/database.ts +392 -0
- package/src/cloudformation/builders/functions.ts +343 -0
- package/src/cloudformation/builders/messaging.ts +140 -0
- package/src/cloudformation/builders/monitoring.ts +300 -0
- package/src/cloudformation/builders/network.ts +264 -0
- package/src/cloudformation/builders/queue.ts +147 -0
- package/src/cloudformation/builders/security.ts +399 -0
- package/src/cloudformation/builders/storage.ts +285 -0
- package/src/cloudformation/index.ts +30 -0
- package/src/cloudformation/types.ts +173 -0
- package/src/compliance/aws-config.ts +543 -0
- package/src/compliance/cloudtrail.ts +376 -0
- package/src/compliance/compliance.test.ts +423 -0
- package/src/compliance/guardduty.ts +446 -0
- package/src/compliance/index.ts +66 -0
- package/src/compliance/security-hub.ts +456 -0
- package/src/containers/build-optimization.ts +416 -0
- package/src/containers/containers.test.ts +508 -0
- package/src/containers/image-scanning.ts +360 -0
- package/src/containers/index.ts +9 -0
- package/src/containers/registry.ts +293 -0
- package/src/containers/service-mesh.ts +520 -0
- package/src/database/database.test.ts +762 -0
- package/src/database/index.ts +9 -0
- package/src/database/migrations.ts +444 -0
- package/src/database/performance.ts +528 -0
- package/src/database/replicas.ts +534 -0
- package/src/database/users.ts +494 -0
- package/src/dependency-graph.ts +143 -0
- package/src/deployment/ab-testing.ts +582 -0
- package/src/deployment/blue-green.ts +452 -0
- package/src/deployment/canary.ts +500 -0
- package/src/deployment/deployment.test.ts +526 -0
- package/src/deployment/index.ts +61 -0
- package/src/deployment/progressive.ts +62 -0
- package/src/dns/dns.test.ts +641 -0
- package/src/dns/dnssec.ts +315 -0
- package/src/dns/index.ts +8 -0
- package/src/dns/resolver.ts +496 -0
- package/src/dns/routing.ts +593 -0
- package/src/email/advanced/analytics.ts +445 -0
- package/src/email/advanced/index.ts +11 -0
- package/src/email/advanced/rules.ts +465 -0
- package/src/email/advanced/scheduling.ts +352 -0
- package/src/email/advanced/search.ts +412 -0
- package/src/email/advanced/shared-mailboxes.ts +404 -0
- package/src/email/advanced/templates.ts +455 -0
- package/src/email/advanced/threading.ts +281 -0
- package/src/email/analytics.ts +467 -0
- package/src/email/bounce-handling.ts +425 -0
- package/src/email/email.test.ts +431 -0
- package/src/email/handlers/__tests__/inbound.test.ts +38 -0
- package/src/email/handlers/__tests__/outbound.test.ts +37 -0
- package/src/email/handlers/converter.ts +227 -0
- package/src/email/handlers/feedback.ts +228 -0
- package/src/email/handlers/inbound.ts +169 -0
- package/src/email/handlers/outbound.ts +178 -0
- package/src/email/index.ts +15 -0
- package/src/email/reputation.ts +303 -0
- package/src/email/templates.ts +352 -0
- package/src/errors/index.test.ts +434 -0
- package/src/errors/index.ts +416 -0
- package/src/health-checks/index.ts +40 -0
- package/src/index.ts +360 -0
- package/src/intrinsic-functions.ts +118 -0
- package/src/lambda/concurrency.ts +330 -0
- package/src/lambda/destinations.ts +345 -0
- package/src/lambda/dlq.ts +425 -0
- package/src/lambda/index.ts +11 -0
- package/src/lambda/lambda.test.ts +840 -0
- package/src/lambda/layers.ts +263 -0
- package/src/lambda/versions.ts +376 -0
- package/src/lambda/vpc.ts +399 -0
- package/src/local/config.ts +114 -0
- package/src/local/index.ts +6 -0
- package/src/local/mock-aws.ts +351 -0
- package/src/modules/ai.ts +340 -0
- package/src/modules/api.ts +478 -0
- package/src/modules/auth.ts +805 -0
- package/src/modules/cache.ts +417 -0
- package/src/modules/cdn.ts +1062 -0
- package/src/modules/communication.ts +1094 -0
- package/src/modules/compute.ts +3348 -0
- package/src/modules/database.ts +554 -0
- package/src/modules/deployment.ts +1079 -0
- package/src/modules/dns.ts +337 -0
- package/src/modules/email.ts +1538 -0
- package/src/modules/filesystem.ts +515 -0
- package/src/modules/index.ts +32 -0
- package/src/modules/messaging.ts +486 -0
- package/src/modules/monitoring.ts +2086 -0
- package/src/modules/network.ts +664 -0
- package/src/modules/parameter-store.ts +325 -0
- package/src/modules/permissions.ts +1081 -0
- package/src/modules/phone.ts +494 -0
- package/src/modules/queue.ts +1260 -0
- package/src/modules/redirects.ts +464 -0
- package/src/modules/registry.ts +699 -0
- package/src/modules/search.ts +401 -0
- package/src/modules/secrets.ts +416 -0
- package/src/modules/security.ts +731 -0
- package/src/modules/sms.ts +389 -0
- package/src/modules/storage.ts +1120 -0
- package/src/modules/workflow.ts +680 -0
- package/src/multi-account/config.ts +521 -0
- package/src/multi-account/index.ts +7 -0
- package/src/multi-account/manager.ts +427 -0
- package/src/multi-region/cross-region.ts +410 -0
- package/src/multi-region/index.ts +8 -0
- package/src/multi-region/manager.ts +483 -0
- package/src/multi-region/regions.ts +435 -0
- package/src/network-security/index.ts +48 -0
- package/src/observability/index.ts +9 -0
- package/src/observability/logs.ts +522 -0
- package/src/observability/metrics.ts +460 -0
- package/src/observability/observability.test.ts +782 -0
- package/src/observability/synthetics.ts +568 -0
- package/src/observability/xray.ts +358 -0
- package/src/phone/advanced/analytics.ts +349 -0
- package/src/phone/advanced/callbacks.ts +428 -0
- package/src/phone/advanced/index.ts +8 -0
- package/src/phone/advanced/ivr-builder.ts +504 -0
- package/src/phone/advanced/recording.ts +310 -0
- package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
- package/src/phone/handlers/incoming-call.ts +117 -0
- package/src/phone/handlers/missed-call.ts +116 -0
- package/src/phone/handlers/voicemail.ts +179 -0
- package/src/phone/index.ts +9 -0
- package/src/presets/api-backend.ts +134 -0
- package/src/presets/data-pipeline.ts +204 -0
- package/src/presets/extend.test.ts +295 -0
- package/src/presets/extend.ts +297 -0
- package/src/presets/fullstack-app.ts +144 -0
- package/src/presets/index.ts +27 -0
- package/src/presets/jamstack.ts +135 -0
- package/src/presets/microservices.ts +167 -0
- package/src/presets/ml-api.ts +208 -0
- package/src/presets/nodejs-server.ts +104 -0
- package/src/presets/nodejs-serverless.ts +114 -0
- package/src/presets/realtime-app.ts +184 -0
- package/src/presets/static-site.ts +64 -0
- package/src/presets/traditional-web-app.ts +339 -0
- package/src/presets/wordpress.ts +138 -0
- package/src/preview/github.test.ts +249 -0
- package/src/preview/github.ts +297 -0
- package/src/preview/index.ts +37 -0
- package/src/preview/manager.test.ts +440 -0
- package/src/preview/manager.ts +326 -0
- package/src/preview/notifications.test.ts +582 -0
- package/src/preview/notifications.ts +341 -0
- package/src/queue/batch-processing.ts +402 -0
- package/src/queue/dlq-monitoring.ts +402 -0
- package/src/queue/fifo.ts +342 -0
- package/src/queue/index.ts +9 -0
- package/src/queue/management.ts +428 -0
- package/src/queue/queue.test.ts +429 -0
- package/src/resource-mgmt/index.ts +39 -0
- package/src/resource-naming.ts +62 -0
- package/src/s3/index.ts +523 -0
- package/src/schema/cloud-config.schema.json +554 -0
- package/src/schema/index.ts +68 -0
- package/src/security/certificate-manager.ts +492 -0
- package/src/security/index.ts +9 -0
- package/src/security/scanning.ts +545 -0
- package/src/security/secrets-manager.ts +476 -0
- package/src/security/secrets-rotation.ts +456 -0
- package/src/security/security.test.ts +738 -0
- package/src/sms/advanced/ab-testing.ts +389 -0
- package/src/sms/advanced/analytics.ts +336 -0
- package/src/sms/advanced/campaigns.ts +523 -0
- package/src/sms/advanced/chatbot.ts +224 -0
- package/src/sms/advanced/index.ts +10 -0
- package/src/sms/advanced/link-tracking.ts +248 -0
- package/src/sms/advanced/mms.ts +308 -0
- package/src/sms/handlers/__tests__/send.test.ts +40 -0
- package/src/sms/handlers/delivery-status.ts +133 -0
- package/src/sms/handlers/receive.ts +162 -0
- package/src/sms/handlers/send.ts +174 -0
- package/src/sms/index.ts +9 -0
- package/src/stack-diff.ts +389 -0
- package/src/static-site/index.ts +85 -0
- package/src/template-builder.ts +110 -0
- package/src/template-validator.ts +574 -0
- package/src/utils/cache.ts +291 -0
- package/src/utils/diff.ts +269 -0
- package/src/utils/hash.ts +227 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/parallel.ts +294 -0
- package/src/validators/credentials.test.ts +274 -0
- package/src/validators/credentials.ts +233 -0
- package/src/validators/quotas.test.ts +434 -0
- package/src/validators/quotas.ts +217 -0
- package/test/ai.test.ts +327 -0
- package/test/api.test.ts +511 -0
- package/test/auth.test.ts +632 -0
- package/test/cache.test.ts +406 -0
- package/test/cdn.test.ts +247 -0
- package/test/compute.test.ts +861 -0
- package/test/database.test.ts +523 -0
- package/test/deployment.test.ts +499 -0
- package/test/dns.test.ts +270 -0
- package/test/email.test.ts +439 -0
- package/test/filesystem.test.ts +382 -0
- package/test/integration.test.ts +350 -0
- package/test/messaging.test.ts +514 -0
- package/test/monitoring.test.ts +634 -0
- package/test/network.test.ts +425 -0
- package/test/permissions.test.ts +488 -0
- package/test/queue.test.ts +484 -0
- package/test/registry.test.ts +306 -0
- package/test/security.test.ts +462 -0
- package/test/storage.test.ts +463 -0
- package/test/template-validator.test.ts +559 -0
- package/test/workflow.test.ts +592 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
import type { CloudFrontDistribution, CloudFrontOriginAccessControl, LambdaFunction, IAMRole } from '@stacksjs/ts-cloud-aws-types'
|
|
2
|
+
import { Fn } from '../intrinsic-functions'
|
|
3
|
+
import { generateLogicalId, generateResourceName } from '../resource-naming'
|
|
4
|
+
import type { EnvironmentType } from '@stacksjs/ts-cloud-types'
|
|
5
|
+
|
|
6
|
+
export interface DistributionOptions {
|
|
7
|
+
slug: string
|
|
8
|
+
environment: EnvironmentType
|
|
9
|
+
origin: OriginConfig
|
|
10
|
+
customDomain?: string
|
|
11
|
+
certificateArn?: string
|
|
12
|
+
errorPages?: ErrorPageMapping[]
|
|
13
|
+
cachePolicy?: CachePolicyConfig
|
|
14
|
+
edgeFunctions?: EdgeFunctionConfig[]
|
|
15
|
+
http3?: boolean
|
|
16
|
+
comment?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface OriginConfig {
|
|
20
|
+
type?: 's3' | 'alb' | 'custom'
|
|
21
|
+
id?: string
|
|
22
|
+
originId?: string // Alias for id
|
|
23
|
+
domainName?: string
|
|
24
|
+
originPath?: string
|
|
25
|
+
customHeaders?: Record<string, string>
|
|
26
|
+
s3OriginAccessControl?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ErrorPageMapping {
|
|
30
|
+
errorCode: number
|
|
31
|
+
responseCode?: number
|
|
32
|
+
responsePagePath?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CachePolicyConfig {
|
|
36
|
+
minTTL?: number
|
|
37
|
+
maxTTL?: number
|
|
38
|
+
defaultTTL?: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface EdgeFunctionConfig {
|
|
42
|
+
event: 'origin-request' | 'origin-response' | 'viewer-request' | 'viewer-response'
|
|
43
|
+
functionArn: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* CDN Module - CloudFront Distribution Management
|
|
48
|
+
* Provides clean API for creating and configuring CloudFront distributions
|
|
49
|
+
*/
|
|
50
|
+
export class CDN {
|
|
51
|
+
/**
|
|
52
|
+
* Create a CloudFront distribution
|
|
53
|
+
*/
|
|
54
|
+
static createDistribution(options: DistributionOptions): {
|
|
55
|
+
distribution: CloudFrontDistribution
|
|
56
|
+
originAccessControl?: CloudFrontOriginAccessControl
|
|
57
|
+
logicalId: string
|
|
58
|
+
} {
|
|
59
|
+
const {
|
|
60
|
+
slug,
|
|
61
|
+
environment,
|
|
62
|
+
origin,
|
|
63
|
+
customDomain,
|
|
64
|
+
certificateArn,
|
|
65
|
+
errorPages,
|
|
66
|
+
cachePolicy,
|
|
67
|
+
edgeFunctions,
|
|
68
|
+
http3 = false,
|
|
69
|
+
comment,
|
|
70
|
+
} = options
|
|
71
|
+
|
|
72
|
+
const resourceName = generateResourceName({
|
|
73
|
+
slug,
|
|
74
|
+
environment,
|
|
75
|
+
resourceType: 'cdn',
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const logicalId = generateLogicalId(resourceName)
|
|
79
|
+
|
|
80
|
+
// Create origin configuration
|
|
81
|
+
const originConfig: any = {
|
|
82
|
+
Id: 'DefaultOrigin',
|
|
83
|
+
DomainName: origin.domainName,
|
|
84
|
+
OriginPath: origin.originPath || '',
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Configure S3 origin with OAC
|
|
88
|
+
let originAccessControl: CloudFrontOriginAccessControl | undefined
|
|
89
|
+
|
|
90
|
+
if (origin.type === 's3') {
|
|
91
|
+
const oacLogicalId = `${logicalId}OAC`
|
|
92
|
+
|
|
93
|
+
originAccessControl = {
|
|
94
|
+
Type: 'AWS::CloudFront::OriginAccessControl',
|
|
95
|
+
Properties: {
|
|
96
|
+
OriginAccessControlConfig: {
|
|
97
|
+
Name: `${resourceName}-oac`,
|
|
98
|
+
Description: `Origin Access Control for ${resourceName}`,
|
|
99
|
+
OriginAccessControlOriginType: 's3',
|
|
100
|
+
SigningBehavior: 'always',
|
|
101
|
+
SigningProtocol: 'sigv4',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
originConfig.OriginAccessControlId = Fn.Ref(oacLogicalId)
|
|
107
|
+
}
|
|
108
|
+
else if (origin.type === 'alb' || origin.type === 'custom') {
|
|
109
|
+
originConfig.CustomOriginConfig = {
|
|
110
|
+
HTTPPort: 80,
|
|
111
|
+
HTTPSPort: 443,
|
|
112
|
+
OriginProtocolPolicy: 'https-only',
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Build distribution
|
|
117
|
+
const distribution: CloudFrontDistribution = {
|
|
118
|
+
Type: 'AWS::CloudFront::Distribution',
|
|
119
|
+
Properties: {
|
|
120
|
+
DistributionConfig: {
|
|
121
|
+
Enabled: true,
|
|
122
|
+
Comment: comment || `CDN for ${resourceName}`,
|
|
123
|
+
DefaultRootObject: 'index.html',
|
|
124
|
+
Origins: [originConfig],
|
|
125
|
+
DefaultCacheBehavior: {
|
|
126
|
+
TargetOriginId: 'DefaultOrigin',
|
|
127
|
+
ViewerProtocolPolicy: 'redirect-to-https',
|
|
128
|
+
AllowedMethods: ['GET', 'HEAD', 'OPTIONS'],
|
|
129
|
+
CachedMethods: ['GET', 'HEAD', 'OPTIONS'],
|
|
130
|
+
Compress: true,
|
|
131
|
+
},
|
|
132
|
+
PriceClass: 'PriceClass_100', // Use only North America and Europe
|
|
133
|
+
HttpVersion: http3 ? 'http2and3' : 'http2',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Configure custom domain and certificate
|
|
139
|
+
if (customDomain && certificateArn) {
|
|
140
|
+
distribution.Properties.DistributionConfig.Aliases = [customDomain]
|
|
141
|
+
distribution.Properties.DistributionConfig.ViewerCertificate = {
|
|
142
|
+
AcmCertificateArn: certificateArn,
|
|
143
|
+
SslSupportMethod: 'sni-only',
|
|
144
|
+
MinimumProtocolVersion: 'TLSv1.2_2021',
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Configure error pages (for SPA routing)
|
|
149
|
+
if (errorPages && errorPages.length > 0) {
|
|
150
|
+
distribution.Properties.DistributionConfig.CustomErrorResponses = errorPages.map(page => ({
|
|
151
|
+
ErrorCode: page.errorCode,
|
|
152
|
+
ResponseCode: page.responseCode,
|
|
153
|
+
ResponsePagePath: page.responsePagePath,
|
|
154
|
+
}))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Configure Lambda@Edge functions
|
|
158
|
+
if (edgeFunctions && edgeFunctions.length > 0) {
|
|
159
|
+
distribution.Properties.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations =
|
|
160
|
+
edgeFunctions.map(fn => ({
|
|
161
|
+
EventType: fn.event,
|
|
162
|
+
LambdaFunctionARN: fn.functionArn,
|
|
163
|
+
}))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
distribution,
|
|
168
|
+
originAccessControl,
|
|
169
|
+
logicalId,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Set custom domain on a distribution
|
|
175
|
+
*/
|
|
176
|
+
static setCustomDomain(
|
|
177
|
+
distribution: CloudFrontDistribution,
|
|
178
|
+
domain: string,
|
|
179
|
+
certificateArn: string,
|
|
180
|
+
): CloudFrontDistribution {
|
|
181
|
+
distribution.Properties.DistributionConfig.Aliases = [domain]
|
|
182
|
+
distribution.Properties.DistributionConfig.ViewerCertificate = {
|
|
183
|
+
AcmCertificateArn: certificateArn,
|
|
184
|
+
SslSupportMethod: 'sni-only',
|
|
185
|
+
MinimumProtocolVersion: 'TLSv1.2_2021',
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return distribution
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Set error pages for SPA routing (404 → index.html)
|
|
193
|
+
*/
|
|
194
|
+
static setErrorPages(
|
|
195
|
+
distribution: CloudFrontDistribution,
|
|
196
|
+
mappings: ErrorPageMapping[],
|
|
197
|
+
): CloudFrontDistribution {
|
|
198
|
+
distribution.Properties.DistributionConfig.CustomErrorResponses = mappings.map(page => ({
|
|
199
|
+
ErrorCode: page.errorCode,
|
|
200
|
+
ResponseCode: page.responseCode,
|
|
201
|
+
ResponsePagePath: page.responsePagePath,
|
|
202
|
+
}))
|
|
203
|
+
|
|
204
|
+
return distribution
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Enable HTTP/3 support
|
|
209
|
+
*/
|
|
210
|
+
static enableHttp3(distribution: CloudFrontDistribution): CloudFrontDistribution {
|
|
211
|
+
distribution.Properties.DistributionConfig.HttpVersion = 'http2and3'
|
|
212
|
+
return distribution
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Add Lambda@Edge function
|
|
217
|
+
*/
|
|
218
|
+
static addEdgeFunction(
|
|
219
|
+
distribution: CloudFrontDistribution,
|
|
220
|
+
event: EdgeFunctionConfig['event'],
|
|
221
|
+
functionArn: string,
|
|
222
|
+
): CloudFrontDistribution {
|
|
223
|
+
if (!distribution.Properties.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations) {
|
|
224
|
+
distribution.Properties.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations = []
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
distribution.Properties.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations.push({
|
|
228
|
+
EventType: event,
|
|
229
|
+
LambdaFunctionARN: functionArn,
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
return distribution
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Set cache policy with custom TTL
|
|
237
|
+
*/
|
|
238
|
+
static setCachePolicy(
|
|
239
|
+
distribution: CloudFrontDistribution,
|
|
240
|
+
ttl: { min?: number, max?: number, default?: number },
|
|
241
|
+
): CloudFrontDistribution {
|
|
242
|
+
// Note: For full cache policy support, we'd need to create a CachePolicy resource
|
|
243
|
+
// For now, we'll just set the comment to indicate the desired TTL
|
|
244
|
+
distribution.Properties.DistributionConfig.Comment =
|
|
245
|
+
`${distribution.Properties.DistributionConfig.Comment || ''} (TTL: ${ttl.default || 86400}s)`
|
|
246
|
+
|
|
247
|
+
return distribution
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Create standard SPA (Single Page Application) configuration
|
|
252
|
+
* Routes all 404/403 errors to index.html
|
|
253
|
+
*/
|
|
254
|
+
static createSpaDistribution(options: Omit<DistributionOptions, 'errorPages'>): ReturnType<typeof CDN.createDistribution> {
|
|
255
|
+
return CDN.createDistribution({
|
|
256
|
+
...options,
|
|
257
|
+
errorPages: [
|
|
258
|
+
{ errorCode: 404, responseCode: 200, responsePagePath: '/index.html' },
|
|
259
|
+
{ errorCode: 403, responseCode: 200, responsePagePath: '/index.html' },
|
|
260
|
+
],
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Create Lambda@Edge origin request function for docs routing
|
|
266
|
+
* Handles:
|
|
267
|
+
* - Pretty URLs (e.g., /guide → /guide.html or /guide/index.html)
|
|
268
|
+
* - Trailing slashes normalization
|
|
269
|
+
* - Default document serving (index.html)
|
|
270
|
+
*/
|
|
271
|
+
static createDocsOriginRequestFunction(options: {
|
|
272
|
+
slug: string
|
|
273
|
+
environment: EnvironmentType
|
|
274
|
+
}): {
|
|
275
|
+
lambdaFunction: LambdaFunction
|
|
276
|
+
role: IAMRole
|
|
277
|
+
functionLogicalId: string
|
|
278
|
+
roleLogicalId: string
|
|
279
|
+
versionLogicalId: string
|
|
280
|
+
} {
|
|
281
|
+
const { slug, environment } = options
|
|
282
|
+
|
|
283
|
+
const resourceName = generateResourceName({
|
|
284
|
+
slug,
|
|
285
|
+
environment,
|
|
286
|
+
resourceType: 'edge-docs',
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
const functionLogicalId = generateLogicalId(resourceName)
|
|
290
|
+
const roleLogicalId = generateLogicalId(`${resourceName}-role`)
|
|
291
|
+
const versionLogicalId = generateLogicalId(`${resourceName}-version`)
|
|
292
|
+
|
|
293
|
+
// Lambda@Edge execution role
|
|
294
|
+
const role: IAMRole = {
|
|
295
|
+
Type: 'AWS::IAM::Role',
|
|
296
|
+
Properties: {
|
|
297
|
+
RoleName: `${resourceName}-role`,
|
|
298
|
+
AssumeRolePolicyDocument: {
|
|
299
|
+
Version: '2012-10-17',
|
|
300
|
+
Statement: [
|
|
301
|
+
{
|
|
302
|
+
Effect: 'Allow',
|
|
303
|
+
Principal: {
|
|
304
|
+
Service: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'],
|
|
305
|
+
},
|
|
306
|
+
Action: 'sts:AssumeRole',
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
ManagedPolicyArns: [
|
|
311
|
+
'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Lambda@Edge function code for docs routing
|
|
317
|
+
// This handles VitePress/docs URL patterns
|
|
318
|
+
const lambdaCode = `
|
|
319
|
+
'use strict';
|
|
320
|
+
|
|
321
|
+
exports.handler = async (event) => {
|
|
322
|
+
const request = event.Records[0].cf.request;
|
|
323
|
+
let uri = request.uri;
|
|
324
|
+
|
|
325
|
+
// If URI ends with a slash, append index.html
|
|
326
|
+
if (uri.endsWith('/')) {
|
|
327
|
+
request.uri = uri + 'index.html';
|
|
328
|
+
return request;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// If URI has a file extension, serve as-is
|
|
332
|
+
if (uri.includes('.')) {
|
|
333
|
+
return request;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Try to determine if this is a directory or a file
|
|
337
|
+
// First, try appending .html (for VitePress clean URLs)
|
|
338
|
+
// If the file doesn't exist, CloudFront will try the directory with index.html
|
|
339
|
+
|
|
340
|
+
// Check if the URI looks like a file path without extension
|
|
341
|
+
const parts = uri.split('/');
|
|
342
|
+
const lastPart = parts[parts.length - 1];
|
|
343
|
+
|
|
344
|
+
// If the last part has no extension, append .html
|
|
345
|
+
if (lastPart && !lastPart.includes('.')) {
|
|
346
|
+
request.uri = uri + '.html';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return request;
|
|
350
|
+
};
|
|
351
|
+
`.trim()
|
|
352
|
+
|
|
353
|
+
const lambdaFunction: LambdaFunction = {
|
|
354
|
+
Type: 'AWS::Lambda::Function',
|
|
355
|
+
Properties: {
|
|
356
|
+
FunctionName: resourceName,
|
|
357
|
+
Description: 'Lambda@Edge origin request handler for docs routing',
|
|
358
|
+
Runtime: 'nodejs20.x',
|
|
359
|
+
Handler: 'index.handler',
|
|
360
|
+
Role: Fn.GetAtt(roleLogicalId, 'Arn') as any,
|
|
361
|
+
Code: {
|
|
362
|
+
ZipFile: lambdaCode,
|
|
363
|
+
},
|
|
364
|
+
MemorySize: 128,
|
|
365
|
+
Timeout: 5,
|
|
366
|
+
},
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
lambdaFunction,
|
|
371
|
+
role,
|
|
372
|
+
functionLogicalId,
|
|
373
|
+
roleLogicalId,
|
|
374
|
+
versionLogicalId,
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Create a docs-specific CloudFront distribution
|
|
380
|
+
* Includes Lambda@Edge for URL rewriting and proper cache settings
|
|
381
|
+
*/
|
|
382
|
+
static createDocsDistribution(options: {
|
|
383
|
+
slug: string
|
|
384
|
+
environment: EnvironmentType
|
|
385
|
+
origin: OriginConfig
|
|
386
|
+
customDomain?: string
|
|
387
|
+
certificateArn?: string
|
|
388
|
+
lambdaEdgeFunctionArn?: string
|
|
389
|
+
}): {
|
|
390
|
+
distribution: CloudFrontDistribution
|
|
391
|
+
originAccessControl?: CloudFrontOriginAccessControl
|
|
392
|
+
logicalId: string
|
|
393
|
+
} {
|
|
394
|
+
const {
|
|
395
|
+
slug,
|
|
396
|
+
environment,
|
|
397
|
+
origin,
|
|
398
|
+
customDomain,
|
|
399
|
+
certificateArn,
|
|
400
|
+
lambdaEdgeFunctionArn,
|
|
401
|
+
} = options
|
|
402
|
+
|
|
403
|
+
// Create base distribution
|
|
404
|
+
const result = CDN.createDistribution({
|
|
405
|
+
slug,
|
|
406
|
+
environment,
|
|
407
|
+
origin,
|
|
408
|
+
customDomain,
|
|
409
|
+
certificateArn,
|
|
410
|
+
comment: `Docs CDN for ${slug}`,
|
|
411
|
+
errorPages: [
|
|
412
|
+
{ errorCode: 404, responseCode: 404, responsePagePath: '/404.html' },
|
|
413
|
+
{ errorCode: 403, responseCode: 403, responsePagePath: '/404.html' },
|
|
414
|
+
],
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
// Add Lambda@Edge function if provided
|
|
418
|
+
if (lambdaEdgeFunctionArn) {
|
|
419
|
+
CDN.addEdgeFunction(result.distribution, 'origin-request', lambdaEdgeFunctionArn)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Optimize cache settings for static docs
|
|
423
|
+
result.distribution.Properties.DistributionConfig.DefaultCacheBehavior.DefaultTTL = 86400 // 1 day
|
|
424
|
+
result.distribution.Properties.DistributionConfig.DefaultCacheBehavior.MaxTTL = 604800 // 1 week
|
|
425
|
+
result.distribution.Properties.DistributionConfig.DefaultCacheBehavior.MinTTL = 0
|
|
426
|
+
|
|
427
|
+
return result
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Create an API distribution with ALB origin
|
|
432
|
+
* Optimized for API traffic (no caching by default, all methods allowed)
|
|
433
|
+
*/
|
|
434
|
+
static createApiDistribution(options: {
|
|
435
|
+
slug: string
|
|
436
|
+
environment: EnvironmentType
|
|
437
|
+
albDomainName: string
|
|
438
|
+
customDomain?: string
|
|
439
|
+
certificateArn?: string
|
|
440
|
+
pathPattern?: string
|
|
441
|
+
forwardHeaders?: string[]
|
|
442
|
+
forwardCookies?: 'none' | 'all' | 'whitelist'
|
|
443
|
+
whitelistedCookies?: string[]
|
|
444
|
+
customOriginHeaders?: Record<string, string>
|
|
445
|
+
}): {
|
|
446
|
+
distribution: CloudFrontDistribution
|
|
447
|
+
logicalId: string
|
|
448
|
+
} {
|
|
449
|
+
const {
|
|
450
|
+
slug,
|
|
451
|
+
environment,
|
|
452
|
+
albDomainName,
|
|
453
|
+
customDomain,
|
|
454
|
+
certificateArn,
|
|
455
|
+
pathPattern = '/api/*',
|
|
456
|
+
forwardHeaders = ['Host', 'Origin', 'Authorization', 'Content-Type', 'Accept'],
|
|
457
|
+
forwardCookies = 'all',
|
|
458
|
+
whitelistedCookies,
|
|
459
|
+
customOriginHeaders = {},
|
|
460
|
+
} = options
|
|
461
|
+
|
|
462
|
+
const resourceName = generateResourceName({
|
|
463
|
+
slug,
|
|
464
|
+
environment,
|
|
465
|
+
resourceType: 'cdn-api',
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
const logicalId = generateLogicalId(resourceName)
|
|
469
|
+
|
|
470
|
+
// Build custom headers for origin
|
|
471
|
+
const originCustomHeaders: any[] = Object.entries(customOriginHeaders).map(([key, value]) => ({
|
|
472
|
+
HeaderName: key,
|
|
473
|
+
HeaderValue: value,
|
|
474
|
+
}))
|
|
475
|
+
|
|
476
|
+
// ALB origin configuration
|
|
477
|
+
const albOrigin: any = {
|
|
478
|
+
Id: 'ALBOrigin',
|
|
479
|
+
DomainName: albDomainName,
|
|
480
|
+
CustomOriginConfig: {
|
|
481
|
+
HTTPPort: 80,
|
|
482
|
+
HTTPSPort: 443,
|
|
483
|
+
OriginProtocolPolicy: 'https-only',
|
|
484
|
+
OriginSSLProtocols: ['TLSv1.2'],
|
|
485
|
+
OriginReadTimeout: 60,
|
|
486
|
+
OriginKeepaliveTimeout: 60,
|
|
487
|
+
},
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (originCustomHeaders.length > 0) {
|
|
491
|
+
albOrigin.OriginCustomHeaders = originCustomHeaders
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Build cookie forwarding config
|
|
495
|
+
let cookieConfig: any = { Forward: forwardCookies }
|
|
496
|
+
if (forwardCookies === 'whitelist' && whitelistedCookies) {
|
|
497
|
+
cookieConfig.WhitelistedNames = whitelistedCookies
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Build distribution
|
|
501
|
+
const distribution: CloudFrontDistribution = {
|
|
502
|
+
Type: 'AWS::CloudFront::Distribution',
|
|
503
|
+
Properties: {
|
|
504
|
+
DistributionConfig: {
|
|
505
|
+
Enabled: true,
|
|
506
|
+
Comment: `API CDN for ${resourceName}`,
|
|
507
|
+
Origins: [albOrigin],
|
|
508
|
+
DefaultCacheBehavior: {
|
|
509
|
+
TargetOriginId: 'ALBOrigin',
|
|
510
|
+
ViewerProtocolPolicy: 'https-only',
|
|
511
|
+
AllowedMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'PATCH', 'DELETE'],
|
|
512
|
+
CachedMethods: ['GET', 'HEAD', 'OPTIONS'],
|
|
513
|
+
Compress: true,
|
|
514
|
+
// No caching for API by default
|
|
515
|
+
DefaultTTL: 0,
|
|
516
|
+
MaxTTL: 0,
|
|
517
|
+
MinTTL: 0,
|
|
518
|
+
ForwardedValues: {
|
|
519
|
+
QueryString: true,
|
|
520
|
+
Headers: forwardHeaders,
|
|
521
|
+
Cookies: cookieConfig,
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
PriceClass: 'PriceClass_100',
|
|
525
|
+
HttpVersion: 'http2',
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Configure custom domain and certificate
|
|
531
|
+
if (customDomain && certificateArn) {
|
|
532
|
+
distribution.Properties.DistributionConfig.Aliases = [customDomain]
|
|
533
|
+
distribution.Properties.DistributionConfig.ViewerCertificate = {
|
|
534
|
+
AcmCertificateArn: certificateArn,
|
|
535
|
+
SslSupportMethod: 'sni-only',
|
|
536
|
+
MinimumProtocolVersion: 'TLSv1.2_2021',
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return { distribution, logicalId }
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Create a multi-origin distribution (S3 for static, ALB for API)
|
|
545
|
+
*/
|
|
546
|
+
static createMultiOriginDistribution(options: {
|
|
547
|
+
slug: string
|
|
548
|
+
environment: EnvironmentType
|
|
549
|
+
s3BucketDomainName: string
|
|
550
|
+
albDomainName: string
|
|
551
|
+
apiPathPattern?: string
|
|
552
|
+
customDomain?: string
|
|
553
|
+
certificateArn?: string
|
|
554
|
+
customOriginHeaders?: Record<string, string>
|
|
555
|
+
}): {
|
|
556
|
+
distribution: CloudFrontDistribution
|
|
557
|
+
originAccessControl: CloudFrontOriginAccessControl
|
|
558
|
+
logicalId: string
|
|
559
|
+
oacLogicalId: string
|
|
560
|
+
} {
|
|
561
|
+
const {
|
|
562
|
+
slug,
|
|
563
|
+
environment,
|
|
564
|
+
s3BucketDomainName,
|
|
565
|
+
albDomainName,
|
|
566
|
+
apiPathPattern = '/api/*',
|
|
567
|
+
customDomain,
|
|
568
|
+
certificateArn,
|
|
569
|
+
customOriginHeaders = {},
|
|
570
|
+
} = options
|
|
571
|
+
|
|
572
|
+
const resourceName = generateResourceName({
|
|
573
|
+
slug,
|
|
574
|
+
environment,
|
|
575
|
+
resourceType: 'cdn',
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
const logicalId = generateLogicalId(resourceName)
|
|
579
|
+
const oacLogicalId = `${logicalId}OAC`
|
|
580
|
+
|
|
581
|
+
// S3 Origin Access Control
|
|
582
|
+
const originAccessControl: CloudFrontOriginAccessControl = {
|
|
583
|
+
Type: 'AWS::CloudFront::OriginAccessControl',
|
|
584
|
+
Properties: {
|
|
585
|
+
OriginAccessControlConfig: {
|
|
586
|
+
Name: `${resourceName}-oac`,
|
|
587
|
+
Description: `Origin Access Control for ${resourceName}`,
|
|
588
|
+
OriginAccessControlOriginType: 's3',
|
|
589
|
+
SigningBehavior: 'always',
|
|
590
|
+
SigningProtocol: 'sigv4',
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Build custom headers for ALB origin
|
|
596
|
+
const originCustomHeaders: any[] = Object.entries(customOriginHeaders).map(([key, value]) => ({
|
|
597
|
+
HeaderName: key,
|
|
598
|
+
HeaderValue: value,
|
|
599
|
+
}))
|
|
600
|
+
|
|
601
|
+
// S3 origin configuration
|
|
602
|
+
const s3Origin: any = {
|
|
603
|
+
Id: 'S3Origin',
|
|
604
|
+
DomainName: s3BucketDomainName,
|
|
605
|
+
OriginAccessControlId: Fn.Ref(oacLogicalId),
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ALB origin configuration
|
|
609
|
+
const albOrigin: any = {
|
|
610
|
+
Id: 'ALBOrigin',
|
|
611
|
+
DomainName: albDomainName,
|
|
612
|
+
CustomOriginConfig: {
|
|
613
|
+
HTTPPort: 80,
|
|
614
|
+
HTTPSPort: 443,
|
|
615
|
+
OriginProtocolPolicy: 'https-only',
|
|
616
|
+
OriginSSLProtocols: ['TLSv1.2'],
|
|
617
|
+
OriginReadTimeout: 60,
|
|
618
|
+
OriginKeepaliveTimeout: 60,
|
|
619
|
+
},
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (originCustomHeaders.length > 0) {
|
|
623
|
+
albOrigin.OriginCustomHeaders = originCustomHeaders
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Build distribution
|
|
627
|
+
const distribution: CloudFrontDistribution = {
|
|
628
|
+
Type: 'AWS::CloudFront::Distribution',
|
|
629
|
+
Properties: {
|
|
630
|
+
DistributionConfig: {
|
|
631
|
+
Enabled: true,
|
|
632
|
+
Comment: `Multi-origin CDN for ${resourceName}`,
|
|
633
|
+
DefaultRootObject: 'index.html',
|
|
634
|
+
Origins: [s3Origin, albOrigin],
|
|
635
|
+
DefaultCacheBehavior: {
|
|
636
|
+
TargetOriginId: 'S3Origin',
|
|
637
|
+
ViewerProtocolPolicy: 'redirect-to-https',
|
|
638
|
+
AllowedMethods: ['GET', 'HEAD', 'OPTIONS'],
|
|
639
|
+
CachedMethods: ['GET', 'HEAD', 'OPTIONS'],
|
|
640
|
+
Compress: true,
|
|
641
|
+
},
|
|
642
|
+
CacheBehaviors: [
|
|
643
|
+
{
|
|
644
|
+
PathPattern: apiPathPattern,
|
|
645
|
+
TargetOriginId: 'ALBOrigin',
|
|
646
|
+
ViewerProtocolPolicy: 'https-only',
|
|
647
|
+
AllowedMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'PATCH', 'DELETE'],
|
|
648
|
+
CachedMethods: ['GET', 'HEAD', 'OPTIONS'],
|
|
649
|
+
Compress: true,
|
|
650
|
+
DefaultTTL: 0,
|
|
651
|
+
MaxTTL: 0,
|
|
652
|
+
MinTTL: 0,
|
|
653
|
+
ForwardedValues: {
|
|
654
|
+
QueryString: true,
|
|
655
|
+
Headers: ['Host', 'Origin', 'Authorization', 'Content-Type', 'Accept'],
|
|
656
|
+
Cookies: { Forward: 'all' },
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
PriceClass: 'PriceClass_100',
|
|
661
|
+
HttpVersion: 'http2',
|
|
662
|
+
CustomErrorResponses: [
|
|
663
|
+
{ ErrorCode: 404, ResponseCode: 200, ResponsePagePath: '/index.html' },
|
|
664
|
+
{ ErrorCode: 403, ResponseCode: 200, ResponsePagePath: '/index.html' },
|
|
665
|
+
],
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Configure custom domain and certificate
|
|
671
|
+
if (customDomain && certificateArn) {
|
|
672
|
+
distribution.Properties.DistributionConfig.Aliases = [customDomain]
|
|
673
|
+
distribution.Properties.DistributionConfig.ViewerCertificate = {
|
|
674
|
+
AcmCertificateArn: certificateArn,
|
|
675
|
+
SslSupportMethod: 'sni-only',
|
|
676
|
+
MinimumProtocolVersion: 'TLSv1.2_2021',
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return { distribution, originAccessControl, logicalId, oacLogicalId }
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Add ALB origin to an existing distribution
|
|
685
|
+
*/
|
|
686
|
+
static addAlbOrigin(
|
|
687
|
+
distribution: CloudFrontDistribution,
|
|
688
|
+
options: {
|
|
689
|
+
originId: string
|
|
690
|
+
domainName: string
|
|
691
|
+
pathPattern: string
|
|
692
|
+
customHeaders?: Record<string, string>
|
|
693
|
+
forwardHeaders?: string[]
|
|
694
|
+
cacheTtl?: { default: number, max: number, min: number }
|
|
695
|
+
},
|
|
696
|
+
): CloudFrontDistribution {
|
|
697
|
+
const {
|
|
698
|
+
originId,
|
|
699
|
+
domainName,
|
|
700
|
+
pathPattern,
|
|
701
|
+
customHeaders = {},
|
|
702
|
+
forwardHeaders = ['Host', 'Origin', 'Authorization', 'Content-Type', 'Accept'],
|
|
703
|
+
cacheTtl = { default: 0, max: 0, min: 0 },
|
|
704
|
+
} = options
|
|
705
|
+
|
|
706
|
+
// Build custom headers
|
|
707
|
+
const originCustomHeaders: any[] = Object.entries(customHeaders).map(([key, value]) => ({
|
|
708
|
+
HeaderName: key,
|
|
709
|
+
HeaderValue: value,
|
|
710
|
+
}))
|
|
711
|
+
|
|
712
|
+
// ALB origin
|
|
713
|
+
const albOrigin: any = {
|
|
714
|
+
Id: originId,
|
|
715
|
+
DomainName: domainName,
|
|
716
|
+
CustomOriginConfig: {
|
|
717
|
+
HTTPPort: 80,
|
|
718
|
+
HTTPSPort: 443,
|
|
719
|
+
OriginProtocolPolicy: 'https-only',
|
|
720
|
+
OriginSSLProtocols: ['TLSv1.2'],
|
|
721
|
+
OriginReadTimeout: 60,
|
|
722
|
+
OriginKeepaliveTimeout: 60,
|
|
723
|
+
},
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (originCustomHeaders.length > 0) {
|
|
727
|
+
albOrigin.OriginCustomHeaders = originCustomHeaders
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Add origin
|
|
731
|
+
if (!distribution.Properties.DistributionConfig.Origins) {
|
|
732
|
+
distribution.Properties.DistributionConfig.Origins = []
|
|
733
|
+
}
|
|
734
|
+
distribution.Properties.DistributionConfig.Origins.push(albOrigin)
|
|
735
|
+
|
|
736
|
+
// Add cache behavior for the path pattern
|
|
737
|
+
if (!distribution.Properties.DistributionConfig.CacheBehaviors) {
|
|
738
|
+
distribution.Properties.DistributionConfig.CacheBehaviors = []
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
distribution.Properties.DistributionConfig.CacheBehaviors.push({
|
|
742
|
+
PathPattern: pathPattern,
|
|
743
|
+
TargetOriginId: originId,
|
|
744
|
+
ViewerProtocolPolicy: 'https-only',
|
|
745
|
+
AllowedMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'PATCH', 'DELETE'],
|
|
746
|
+
CachedMethods: ['GET', 'HEAD', 'OPTIONS'],
|
|
747
|
+
Compress: true,
|
|
748
|
+
DefaultTTL: cacheTtl.default,
|
|
749
|
+
MaxTTL: cacheTtl.max,
|
|
750
|
+
MinTTL: cacheTtl.min,
|
|
751
|
+
ForwardedValues: {
|
|
752
|
+
QueryString: true,
|
|
753
|
+
Headers: forwardHeaders,
|
|
754
|
+
Cookies: { Forward: 'all' },
|
|
755
|
+
},
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
return distribution
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Add a custom origin header (for origin authentication)
|
|
763
|
+
*/
|
|
764
|
+
static addOriginHeader(
|
|
765
|
+
distribution: CloudFrontDistribution,
|
|
766
|
+
originId: string,
|
|
767
|
+
headerName: string,
|
|
768
|
+
headerValue: string,
|
|
769
|
+
): CloudFrontDistribution {
|
|
770
|
+
const origin = distribution.Properties.DistributionConfig.Origins?.find(
|
|
771
|
+
(o: any) => o.Id === originId,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
if (origin) {
|
|
775
|
+
if (!origin.OriginCustomHeaders) {
|
|
776
|
+
origin.OriginCustomHeaders = []
|
|
777
|
+
}
|
|
778
|
+
origin.OriginCustomHeaders.push({
|
|
779
|
+
HeaderName: headerName,
|
|
780
|
+
HeaderValue: headerValue,
|
|
781
|
+
})
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return distribution
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Lambda@Edge code templates for common use cases
|
|
789
|
+
*/
|
|
790
|
+
static readonly EdgeFunctionTemplates = {
|
|
791
|
+
/**
|
|
792
|
+
* Origin request handler for docs/VitePress routing
|
|
793
|
+
*/
|
|
794
|
+
docsOriginRequest: (`
|
|
795
|
+
'use strict';
|
|
796
|
+
exports.handler = async (event) => {
|
|
797
|
+
const request = event.Records[0].cf.request;
|
|
798
|
+
let uri = request.uri;
|
|
799
|
+
|
|
800
|
+
if (uri.endsWith('/')) {
|
|
801
|
+
request.uri = uri + 'index.html';
|
|
802
|
+
} else if (!uri.includes('.')) {
|
|
803
|
+
request.uri = uri + '.html';
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return request;
|
|
807
|
+
};
|
|
808
|
+
`).trim() as string,
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Viewer response handler for security headers
|
|
812
|
+
*/
|
|
813
|
+
securityHeaders: (`
|
|
814
|
+
'use strict';
|
|
815
|
+
exports.handler = async (event) => {
|
|
816
|
+
const response = event.Records[0].cf.response;
|
|
817
|
+
const headers = response.headers;
|
|
818
|
+
|
|
819
|
+
headers['strict-transport-security'] = [{ value: 'max-age=31536000; includeSubdomains; preload' }];
|
|
820
|
+
headers['x-content-type-options'] = [{ value: 'nosniff' }];
|
|
821
|
+
headers['x-frame-options'] = [{ value: 'DENY' }];
|
|
822
|
+
headers['x-xss-protection'] = [{ value: '1; mode=block' }];
|
|
823
|
+
headers['referrer-policy'] = [{ value: 'strict-origin-when-cross-origin' }];
|
|
824
|
+
|
|
825
|
+
return response;
|
|
826
|
+
};
|
|
827
|
+
`).trim() as string,
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Viewer request handler for basic auth (staging/preview environments)
|
|
831
|
+
*/
|
|
832
|
+
basicAuth: (username: string, password: string): string => `
|
|
833
|
+
'use strict';
|
|
834
|
+
exports.handler = async (event) => {
|
|
835
|
+
const request = event.Records[0].cf.request;
|
|
836
|
+
const headers = request.headers;
|
|
837
|
+
|
|
838
|
+
const authString = 'Basic ' + Buffer.from('${username}:${password}').toString('base64');
|
|
839
|
+
|
|
840
|
+
if (!headers.authorization || headers.authorization[0].value !== authString) {
|
|
841
|
+
return {
|
|
842
|
+
status: '401',
|
|
843
|
+
statusDescription: 'Unauthorized',
|
|
844
|
+
body: 'Unauthorized',
|
|
845
|
+
headers: {
|
|
846
|
+
'www-authenticate': [{ value: 'Basic realm="Protected"' }],
|
|
847
|
+
},
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return request;
|
|
852
|
+
};
|
|
853
|
+
`.trim(),
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Origin request handler for path-based routing (e.g., /api to different origin)
|
|
857
|
+
*/
|
|
858
|
+
pathBasedRouting: (pathPrefix: string, targetOriginId: string): string => `
|
|
859
|
+
'use strict';
|
|
860
|
+
exports.handler = async (event) => {
|
|
861
|
+
const request = event.Records[0].cf.request;
|
|
862
|
+
|
|
863
|
+
if (request.uri.startsWith('${pathPrefix}')) {
|
|
864
|
+
request.origin = {
|
|
865
|
+
custom: {
|
|
866
|
+
domainName: request.headers.host[0].value,
|
|
867
|
+
port: 443,
|
|
868
|
+
protocol: 'https',
|
|
869
|
+
sslProtocols: ['TLSv1.2'],
|
|
870
|
+
},
|
|
871
|
+
};
|
|
872
|
+
// Remove the path prefix for the origin request
|
|
873
|
+
request.uri = request.uri.substring(${pathPrefix.length});
|
|
874
|
+
if (!request.uri.startsWith('/')) {
|
|
875
|
+
request.uri = '/' + request.uri;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return request;
|
|
880
|
+
};
|
|
881
|
+
`.trim(),
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* CDN Configuration helpers
|
|
886
|
+
* Provides Stacks configuration parity for CDN options
|
|
887
|
+
*/
|
|
888
|
+
static readonly Config = {
|
|
889
|
+
/**
|
|
890
|
+
* Create TTL configuration
|
|
891
|
+
*/
|
|
892
|
+
ttl: (options: {
|
|
893
|
+
min?: number
|
|
894
|
+
max?: number
|
|
895
|
+
default?: number
|
|
896
|
+
}): {
|
|
897
|
+
MinTTL: number
|
|
898
|
+
MaxTTL: number
|
|
899
|
+
DefaultTTL: number
|
|
900
|
+
} => {
|
|
901
|
+
const {
|
|
902
|
+
min = 0,
|
|
903
|
+
max = 86400,
|
|
904
|
+
default: defaultTtl = 86400,
|
|
905
|
+
} = options
|
|
906
|
+
|
|
907
|
+
return {
|
|
908
|
+
MinTTL: min,
|
|
909
|
+
MaxTTL: max,
|
|
910
|
+
DefaultTTL: defaultTtl,
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Cookie behavior configuration
|
|
916
|
+
*/
|
|
917
|
+
cookies: (behavior: 'none' | 'all' | 'allowList', allowedCookies?: string[]): {
|
|
918
|
+
Forward: string
|
|
919
|
+
WhitelistedNames?: string[]
|
|
920
|
+
} => {
|
|
921
|
+
const config: any = { Forward: behavior === 'allowList' ? 'whitelist' : behavior }
|
|
922
|
+
if (behavior === 'allowList' && allowedCookies) {
|
|
923
|
+
config.WhitelistedNames = allowedCookies
|
|
924
|
+
}
|
|
925
|
+
return config
|
|
926
|
+
},
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Allowed HTTP methods configuration
|
|
930
|
+
*/
|
|
931
|
+
allowedMethods: (methods: 'ALL' | 'GET_HEAD' | 'GET_HEAD_OPTIONS'): string[] => {
|
|
932
|
+
const mapping: Record<string, string[]> = {
|
|
933
|
+
ALL: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'PATCH', 'DELETE'],
|
|
934
|
+
GET_HEAD: ['GET', 'HEAD'],
|
|
935
|
+
GET_HEAD_OPTIONS: ['GET', 'HEAD', 'OPTIONS'],
|
|
936
|
+
}
|
|
937
|
+
return mapping[methods] || mapping.GET_HEAD
|
|
938
|
+
},
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Cached methods configuration
|
|
942
|
+
*/
|
|
943
|
+
cachedMethods: (methods: 'GET_HEAD' | 'GET_HEAD_OPTIONS'): string[] => {
|
|
944
|
+
const mapping: Record<string, string[]> = {
|
|
945
|
+
GET_HEAD: ['GET', 'HEAD'],
|
|
946
|
+
GET_HEAD_OPTIONS: ['GET', 'HEAD', 'OPTIONS'],
|
|
947
|
+
}
|
|
948
|
+
return mapping[methods] || mapping.GET_HEAD
|
|
949
|
+
},
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Common TTL presets
|
|
953
|
+
*/
|
|
954
|
+
ttlPresets: {
|
|
955
|
+
/** Static assets (1 year) */
|
|
956
|
+
static: { min: 0, max: 31536000, default: 31536000 },
|
|
957
|
+
/** Dynamic content (no cache) */
|
|
958
|
+
dynamic: { min: 0, max: 0, default: 0 },
|
|
959
|
+
/** API responses (1 hour) */
|
|
960
|
+
api: { min: 0, max: 3600, default: 60 },
|
|
961
|
+
/** SPA/HTML (1 day) */
|
|
962
|
+
html: { min: 0, max: 86400, default: 86400 },
|
|
963
|
+
/** Images (1 week) */
|
|
964
|
+
images: { min: 0, max: 604800, default: 604800 },
|
|
965
|
+
} as const,
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Create cache behavior configuration
|
|
969
|
+
*/
|
|
970
|
+
cacheBehavior: (options: {
|
|
971
|
+
ttl?: { min: number, max: number, default: number }
|
|
972
|
+
cookies?: 'none' | 'all' | 'allowList'
|
|
973
|
+
allowedCookies?: string[]
|
|
974
|
+
allowedMethods?: 'ALL' | 'GET_HEAD' | 'GET_HEAD_OPTIONS'
|
|
975
|
+
cachedMethods?: 'GET_HEAD' | 'GET_HEAD_OPTIONS'
|
|
976
|
+
compress?: boolean
|
|
977
|
+
forwardQueryString?: boolean
|
|
978
|
+
forwardHeaders?: string[]
|
|
979
|
+
}): {
|
|
980
|
+
MinTTL: number
|
|
981
|
+
MaxTTL: number
|
|
982
|
+
DefaultTTL: number
|
|
983
|
+
Compress: boolean
|
|
984
|
+
AllowedMethods: string[]
|
|
985
|
+
CachedMethods: string[]
|
|
986
|
+
ForwardedValues: {
|
|
987
|
+
QueryString: boolean
|
|
988
|
+
Headers: string[]
|
|
989
|
+
Cookies: { Forward: string, WhitelistedNames?: string[] }
|
|
990
|
+
}
|
|
991
|
+
} => {
|
|
992
|
+
const {
|
|
993
|
+
ttl = { min: 0, max: 86400, default: 86400 },
|
|
994
|
+
cookies = 'none',
|
|
995
|
+
allowedCookies,
|
|
996
|
+
allowedMethods = 'GET_HEAD',
|
|
997
|
+
cachedMethods = 'GET_HEAD',
|
|
998
|
+
compress = true,
|
|
999
|
+
forwardQueryString = true,
|
|
1000
|
+
forwardHeaders = [],
|
|
1001
|
+
} = options
|
|
1002
|
+
|
|
1003
|
+
return {
|
|
1004
|
+
MinTTL: ttl.min,
|
|
1005
|
+
MaxTTL: ttl.max,
|
|
1006
|
+
DefaultTTL: ttl.default,
|
|
1007
|
+
Compress: compress,
|
|
1008
|
+
AllowedMethods: CDN.Config.allowedMethods(allowedMethods),
|
|
1009
|
+
CachedMethods: CDN.Config.cachedMethods(cachedMethods),
|
|
1010
|
+
ForwardedValues: {
|
|
1011
|
+
QueryString: forwardQueryString,
|
|
1012
|
+
Headers: forwardHeaders,
|
|
1013
|
+
Cookies: CDN.Config.cookies(cookies, allowedCookies),
|
|
1014
|
+
},
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Apply configuration to an existing distribution
|
|
1021
|
+
*/
|
|
1022
|
+
static applyConfig(
|
|
1023
|
+
distribution: CloudFrontDistribution,
|
|
1024
|
+
config: {
|
|
1025
|
+
ttl?: { min: number, max: number, default: number }
|
|
1026
|
+
cookies?: 'none' | 'all' | 'allowList'
|
|
1027
|
+
allowedCookies?: string[]
|
|
1028
|
+
allowedMethods?: 'ALL' | 'GET_HEAD' | 'GET_HEAD_OPTIONS'
|
|
1029
|
+
cachedMethods?: 'GET_HEAD' | 'GET_HEAD_OPTIONS'
|
|
1030
|
+
compress?: boolean
|
|
1031
|
+
},
|
|
1032
|
+
): CloudFrontDistribution {
|
|
1033
|
+
const behavior = distribution.Properties.DistributionConfig.DefaultCacheBehavior
|
|
1034
|
+
|
|
1035
|
+
if (config.ttl) {
|
|
1036
|
+
behavior.MinTTL = config.ttl.min
|
|
1037
|
+
behavior.MaxTTL = config.ttl.max
|
|
1038
|
+
behavior.DefaultTTL = config.ttl.default
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (config.compress !== undefined) {
|
|
1042
|
+
behavior.Compress = config.compress
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (config.allowedMethods) {
|
|
1046
|
+
behavior.AllowedMethods = CDN.Config.allowedMethods(config.allowedMethods)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (config.cachedMethods) {
|
|
1050
|
+
behavior.CachedMethods = CDN.Config.cachedMethods(config.cachedMethods)
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (config.cookies) {
|
|
1054
|
+
if (!behavior.ForwardedValues) {
|
|
1055
|
+
behavior.ForwardedValues = { QueryString: true }
|
|
1056
|
+
}
|
|
1057
|
+
behavior.ForwardedValues.Cookies = CDN.Config.cookies(config.cookies, config.allowedCookies)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return distribution
|
|
1061
|
+
}
|
|
1062
|
+
}
|