Skip to content

Commit 17ef490

Browse files
committed
chore: wip
1 parent f069122 commit 17ef490

10 files changed

Lines changed: 2518 additions & 34 deletions

File tree

packages/core/src/modules/cdn.ts

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import type { CloudFrontDistribution, CloudFrontOriginAccessControl } from '@ts-cloud/aws-types'
2+
import { Fn } from '../intrinsic-functions'
3+
import { generateLogicalId, generateResourceName } from '../resource-naming'
4+
import type { EnvironmentType } from '@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+
domainName: string
22+
originPath?: string
23+
customHeaders?: Record<string, string>
24+
s3OriginAccessControl?: string
25+
}
26+
27+
export interface ErrorPageMapping {
28+
errorCode: number
29+
responseCode?: number
30+
responsePagePath?: string
31+
}
32+
33+
export interface CachePolicyConfig {
34+
minTTL?: number
35+
maxTTL?: number
36+
defaultTTL?: number
37+
}
38+
39+
export interface EdgeFunctionConfig {
40+
event: 'origin-request' | 'origin-response' | 'viewer-request' | 'viewer-response'
41+
functionArn: string
42+
}
43+
44+
/**
45+
* CDN Module - CloudFront Distribution Management
46+
* Provides clean API for creating and configuring CloudFront distributions
47+
*/
48+
export class CDN {
49+
/**
50+
* Create a CloudFront distribution
51+
*/
52+
static createDistribution(options: DistributionOptions): {
53+
distribution: CloudFrontDistribution
54+
originAccessControl?: CloudFrontOriginAccessControl
55+
logicalId: string
56+
} {
57+
const {
58+
slug,
59+
environment,
60+
origin,
61+
customDomain,
62+
certificateArn,
63+
errorPages,
64+
cachePolicy,
65+
edgeFunctions,
66+
http3 = false,
67+
comment,
68+
} = options
69+
70+
const resourceName = generateResourceName({
71+
slug,
72+
environment,
73+
resourceType: 'cdn',
74+
})
75+
76+
const logicalId = generateLogicalId(resourceName)
77+
78+
// Create origin configuration
79+
const originConfig: any = {
80+
Id: 'DefaultOrigin',
81+
DomainName: origin.domainName,
82+
OriginPath: origin.originPath || '',
83+
}
84+
85+
// Configure S3 origin with OAC
86+
let originAccessControl: CloudFrontOriginAccessControl | undefined
87+
88+
if (origin.type === 's3') {
89+
const oacLogicalId = `${logicalId}OAC`
90+
91+
originAccessControl = {
92+
Type: 'AWS::CloudFront::OriginAccessControl',
93+
Properties: {
94+
OriginAccessControlConfig: {
95+
Name: `${resourceName}-oac`,
96+
Description: `Origin Access Control for ${resourceName}`,
97+
OriginAccessControlOriginType: 's3',
98+
SigningBehavior: 'always',
99+
SigningProtocol: 'sigv4',
100+
},
101+
},
102+
}
103+
104+
originConfig.OriginAccessControlId = Fn.Ref(oacLogicalId)
105+
}
106+
else if (origin.type === 'alb' || origin.type === 'custom') {
107+
originConfig.CustomOriginConfig = {
108+
HTTPPort: 80,
109+
HTTPSPort: 443,
110+
OriginProtocolPolicy: 'https-only',
111+
}
112+
}
113+
114+
// Build distribution
115+
const distribution: CloudFrontDistribution = {
116+
Type: 'AWS::CloudFront::Distribution',
117+
Properties: {
118+
DistributionConfig: {
119+
Enabled: true,
120+
Comment: comment || `CDN for ${resourceName}`,
121+
DefaultRootObject: 'index.html',
122+
Origins: [originConfig],
123+
DefaultCacheBehavior: {
124+
TargetOriginId: 'DefaultOrigin',
125+
ViewerProtocolPolicy: 'redirect-to-https',
126+
AllowedMethods: ['GET', 'HEAD', 'OPTIONS'],
127+
CachedMethods: ['GET', 'HEAD', 'OPTIONS'],
128+
Compress: true,
129+
},
130+
PriceClass: 'PriceClass_100', // Use only North America and Europe
131+
HttpVersion: http3 ? 'http2and3' : 'http2',
132+
},
133+
},
134+
}
135+
136+
// Configure custom domain and certificate
137+
if (customDomain && certificateArn) {
138+
distribution.Properties.DistributionConfig.Aliases = [customDomain]
139+
distribution.Properties.DistributionConfig.ViewerCertificate = {
140+
AcmCertificateArn: certificateArn,
141+
SslSupportMethod: 'sni-only',
142+
MinimumProtocolVersion: 'TLSv1.2_2021',
143+
}
144+
}
145+
146+
// Configure error pages (for SPA routing)
147+
if (errorPages && errorPages.length > 0) {
148+
distribution.Properties.DistributionConfig.CustomErrorResponses = errorPages.map(page => ({
149+
ErrorCode: page.errorCode,
150+
ResponseCode: page.responseCode,
151+
ResponsePagePath: page.responsePagePath,
152+
}))
153+
}
154+
155+
// Configure Lambda@Edge functions
156+
if (edgeFunctions && edgeFunctions.length > 0) {
157+
distribution.Properties.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations =
158+
edgeFunctions.map(fn => ({
159+
EventType: fn.event,
160+
LambdaFunctionARN: fn.functionArn,
161+
}))
162+
}
163+
164+
return {
165+
distribution,
166+
originAccessControl,
167+
logicalId,
168+
}
169+
}
170+
171+
/**
172+
* Set custom domain on a distribution
173+
*/
174+
static setCustomDomain(
175+
distribution: CloudFrontDistribution,
176+
domain: string,
177+
certificateArn: string,
178+
): CloudFrontDistribution {
179+
distribution.Properties.DistributionConfig.Aliases = [domain]
180+
distribution.Properties.DistributionConfig.ViewerCertificate = {
181+
AcmCertificateArn: certificateArn,
182+
SslSupportMethod: 'sni-only',
183+
MinimumProtocolVersion: 'TLSv1.2_2021',
184+
}
185+
186+
return distribution
187+
}
188+
189+
/**
190+
* Set error pages for SPA routing (404 → index.html)
191+
*/
192+
static setErrorPages(
193+
distribution: CloudFrontDistribution,
194+
mappings: ErrorPageMapping[],
195+
): CloudFrontDistribution {
196+
distribution.Properties.DistributionConfig.CustomErrorResponses = mappings.map(page => ({
197+
ErrorCode: page.errorCode,
198+
ResponseCode: page.responseCode,
199+
ResponsePagePath: page.responsePagePath,
200+
}))
201+
202+
return distribution
203+
}
204+
205+
/**
206+
* Enable HTTP/3 support
207+
*/
208+
static enableHttp3(distribution: CloudFrontDistribution): CloudFrontDistribution {
209+
distribution.Properties.DistributionConfig.HttpVersion = 'http2and3'
210+
return distribution
211+
}
212+
213+
/**
214+
* Add Lambda@Edge function
215+
*/
216+
static addEdgeFunction(
217+
distribution: CloudFrontDistribution,
218+
event: EdgeFunctionConfig['event'],
219+
functionArn: string,
220+
): CloudFrontDistribution {
221+
if (!distribution.Properties.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations) {
222+
distribution.Properties.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations = []
223+
}
224+
225+
distribution.Properties.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations.push({
226+
EventType: event,
227+
LambdaFunctionARN: functionArn,
228+
})
229+
230+
return distribution
231+
}
232+
233+
/**
234+
* Set cache policy with custom TTL
235+
*/
236+
static setCachePolicy(
237+
distribution: CloudFrontDistribution,
238+
ttl: { min?: number, max?: number, default?: number },
239+
): CloudFrontDistribution {
240+
// Note: For full cache policy support, we'd need to create a CachePolicy resource
241+
// For now, we'll just set the comment to indicate the desired TTL
242+
distribution.Properties.DistributionConfig.Comment =
243+
`${distribution.Properties.DistributionConfig.Comment || ''} (TTL: ${ttl.default || 86400}s)`
244+
245+
return distribution
246+
}
247+
248+
/**
249+
* Create standard SPA (Single Page Application) configuration
250+
* Routes all 404/403 errors to index.html
251+
*/
252+
static createSpaDistribution(options: Omit<DistributionOptions, 'errorPages'>): ReturnType<typeof CDN.createDistribution> {
253+
return CDN.createDistribution({
254+
...options,
255+
errorPages: [
256+
{ errorCode: 404, responseCode: 200, responsePagePath: '/index.html' },
257+
{ errorCode: 403, responseCode: 200, responsePagePath: '/index.html' },
258+
],
259+
})
260+
}
261+
}

0 commit comments

Comments
 (0)