@stacksjs/ts-cloud 0.1.7 → 0.1.9
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/aws/s3.d.ts +1 -1
- package/dist/bin/cli.js +223 -222
- package/dist/index.js +132 -132
- package/package.json +18 -16
- package/src/aws/acm.ts +768 -0
- package/src/aws/application-autoscaling.ts +845 -0
- package/src/aws/bedrock.ts +4074 -0
- package/src/aws/client.ts +891 -0
- package/src/aws/cloudformation.ts +896 -0
- package/src/aws/cloudfront.ts +1531 -0
- package/src/aws/cloudwatch-logs.ts +154 -0
- package/src/aws/comprehend.ts +839 -0
- package/src/aws/connect.ts +1056 -0
- package/src/aws/deploy-imap.ts +384 -0
- package/src/aws/dynamodb.ts +340 -0
- package/src/aws/ec2.ts +1385 -0
- package/src/aws/ecr.ts +621 -0
- package/src/aws/ecs.ts +615 -0
- package/src/aws/elasticache.ts +301 -0
- package/src/aws/elbv2.ts +942 -0
- package/src/aws/email.ts +928 -0
- package/src/aws/eventbridge.ts +248 -0
- package/src/aws/iam.ts +1689 -0
- package/src/aws/imap-server.ts +2100 -0
- package/src/aws/index.ts +213 -0
- package/src/aws/kendra.ts +1097 -0
- package/src/aws/lambda.ts +786 -0
- package/src/aws/opensearch.ts +158 -0
- package/src/aws/personalize.ts +977 -0
- package/src/aws/polly.ts +559 -0
- package/src/aws/rds.ts +888 -0
- package/src/aws/rekognition.ts +846 -0
- package/src/aws/route53-domains.ts +359 -0
- package/src/aws/route53.ts +1046 -0
- package/src/aws/s3.ts +2334 -0
- package/src/aws/scheduler.ts +571 -0
- package/src/aws/secrets-manager.ts +769 -0
- package/src/aws/ses.ts +1081 -0
- package/src/aws/setup-phone.ts +104 -0
- package/src/aws/setup-sms.ts +580 -0
- package/src/aws/sms.ts +1735 -0
- package/src/aws/smtp-server.ts +531 -0
- package/src/aws/sns.ts +758 -0
- package/src/aws/sqs.ts +382 -0
- package/src/aws/ssm.ts +807 -0
- package/src/aws/sts.ts +92 -0
- package/src/aws/support.ts +391 -0
- package/src/aws/test-imap.ts +86 -0
- package/src/aws/textract.ts +780 -0
- package/src/aws/transcribe.ts +108 -0
- package/src/aws/translate.ts +641 -0
- package/src/aws/voice.ts +1379 -0
- package/src/config.ts +35 -0
- package/src/deploy/index.ts +7 -0
- package/src/deploy/static-site-external-dns.ts +945 -0
- package/src/deploy/static-site.ts +1175 -0
- package/src/dns/cloudflare.ts +548 -0
- package/src/dns/godaddy.ts +412 -0
- package/src/dns/index.ts +205 -0
- package/src/dns/porkbun.ts +362 -0
- package/src/dns/route53-adapter.ts +414 -0
- package/src/dns/types.ts +119 -0
- package/src/dns/validator.ts +369 -0
- package/src/generators/index.ts +5 -0
- package/src/generators/infrastructure.ts +1660 -0
- package/src/index.ts +163 -0
- package/src/push/apns.ts +452 -0
- package/src/push/fcm.ts +506 -0
- package/src/push/index.ts +58 -0
- package/src/security/pre-deploy-scanner.ts +655 -0
- package/src/ssl/acme-client.ts +478 -0
- package/src/ssl/index.ts +7 -0
- package/src/ssl/letsencrypt.ts +747 -0
- package/src/types.ts +2 -0
- package/src/utils/cli.ts +398 -0
- package/src/validation/index.ts +5 -0
- package/src/validation/template.ts +405 -0
|
@@ -0,0 +1,1531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS CloudFront Operations
|
|
3
|
+
* Direct API calls without AWS CLI dependency
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AWSClient } from './client'
|
|
7
|
+
|
|
8
|
+
export interface InvalidationOptions {
|
|
9
|
+
distributionId: string
|
|
10
|
+
paths: string[]
|
|
11
|
+
callerReference?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Distribution {
|
|
15
|
+
Id: string
|
|
16
|
+
ARN: string
|
|
17
|
+
Status: string
|
|
18
|
+
DomainName: string
|
|
19
|
+
Aliases?: { Quantity?: number; Items?: string[] }
|
|
20
|
+
Enabled: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* CloudFront client using direct API calls
|
|
25
|
+
*/
|
|
26
|
+
export class CloudFrontClient {
|
|
27
|
+
private client: AWSClient
|
|
28
|
+
|
|
29
|
+
constructor(profile?: string) {
|
|
30
|
+
this.client = new AWSClient()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create cache invalidation
|
|
35
|
+
*/
|
|
36
|
+
async createInvalidation(options: InvalidationOptions): Promise<{
|
|
37
|
+
Id: string
|
|
38
|
+
Status: string
|
|
39
|
+
CreateTime: string
|
|
40
|
+
}> {
|
|
41
|
+
const callerReference = options.callerReference || Date.now().toString()
|
|
42
|
+
|
|
43
|
+
const invalidationBatchXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
44
|
+
<InvalidationBatch>
|
|
45
|
+
<Paths>
|
|
46
|
+
<Quantity>${options.paths.length}</Quantity>
|
|
47
|
+
<Items>
|
|
48
|
+
${options.paths.map(path => `<Path>${path}</Path>`).join('\n ')}
|
|
49
|
+
</Items>
|
|
50
|
+
</Paths>
|
|
51
|
+
<CallerReference>${callerReference}</CallerReference>
|
|
52
|
+
</InvalidationBatch>`
|
|
53
|
+
|
|
54
|
+
const result = await this.client.request({
|
|
55
|
+
service: 'cloudfront',
|
|
56
|
+
region: 'us-east-1', // CloudFront is global
|
|
57
|
+
method: 'POST',
|
|
58
|
+
path: `/2020-05-31/distribution/${options.distributionId}/invalidation`,
|
|
59
|
+
body: invalidationBatchXml,
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'application/xml',
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
Id: result.Id || result.Invalidation?.Id,
|
|
67
|
+
Status: result.Status || result.Invalidation?.Status || 'InProgress',
|
|
68
|
+
CreateTime: result.CreateTime || result.Invalidation?.CreateTime || new Date().toISOString(),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get invalidation status
|
|
74
|
+
*/
|
|
75
|
+
async getInvalidation(distributionId: string, invalidationId: string): Promise<{
|
|
76
|
+
Id: string
|
|
77
|
+
Status: string
|
|
78
|
+
CreateTime: string
|
|
79
|
+
}> {
|
|
80
|
+
const result = await this.client.request({
|
|
81
|
+
service: 'cloudfront',
|
|
82
|
+
region: 'us-east-1',
|
|
83
|
+
method: 'GET',
|
|
84
|
+
path: `/2020-05-31/distribution/${distributionId}/invalidation/${invalidationId}`,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
Id: result.Id || result.Invalidation?.Id,
|
|
89
|
+
Status: result.Status || result.Invalidation?.Status,
|
|
90
|
+
CreateTime: result.CreateTime || result.Invalidation?.CreateTime,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* List invalidations
|
|
96
|
+
*/
|
|
97
|
+
async listInvalidations(distributionId: string): Promise<Array<{
|
|
98
|
+
Id: string
|
|
99
|
+
Status: string
|
|
100
|
+
CreateTime: string
|
|
101
|
+
}>> {
|
|
102
|
+
const result = await this.client.request({
|
|
103
|
+
service: 'cloudfront',
|
|
104
|
+
region: 'us-east-1',
|
|
105
|
+
method: 'GET',
|
|
106
|
+
path: `/2020-05-31/distribution/${distributionId}/invalidation`,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Parse invalidation list
|
|
110
|
+
const invalidations: Array<{ Id: string, Status: string, CreateTime: string }> = []
|
|
111
|
+
|
|
112
|
+
// Simple parser - would need proper XML parsing in production
|
|
113
|
+
if (result.InvalidationSummary) {
|
|
114
|
+
const summaries = Array.isArray(result.InvalidationSummary)
|
|
115
|
+
? result.InvalidationSummary
|
|
116
|
+
: [result.InvalidationSummary]
|
|
117
|
+
|
|
118
|
+
invalidations.push(...summaries.map((item: any) => ({
|
|
119
|
+
Id: item.Id,
|
|
120
|
+
Status: item.Status,
|
|
121
|
+
CreateTime: item.CreateTime,
|
|
122
|
+
})))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return invalidations
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Wait for invalidation to complete
|
|
130
|
+
*/
|
|
131
|
+
async waitForInvalidation(distributionId: string, invalidationId: string): Promise<void> {
|
|
132
|
+
const maxAttempts = 60 // 5 minutes
|
|
133
|
+
let attempts = 0
|
|
134
|
+
|
|
135
|
+
while (attempts < maxAttempts) {
|
|
136
|
+
const invalidation = await this.getInvalidation(distributionId, invalidationId)
|
|
137
|
+
|
|
138
|
+
if (invalidation.Status === 'Completed') {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Wait 5 seconds before next attempt
|
|
143
|
+
await new Promise(resolve => setTimeout(resolve, 5000))
|
|
144
|
+
attempts++
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new Error(`Timeout waiting for invalidation ${invalidationId} to complete`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* List distributions
|
|
152
|
+
*/
|
|
153
|
+
async listDistributions(): Promise<Distribution[]> {
|
|
154
|
+
const result = await this.client.request({
|
|
155
|
+
service: 'cloudfront',
|
|
156
|
+
region: 'us-east-1',
|
|
157
|
+
method: 'GET',
|
|
158
|
+
path: '/2020-05-31/distribution',
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const distributions: Distribution[] = []
|
|
162
|
+
|
|
163
|
+
// The response structure is: DistributionList.Items.DistributionSummary
|
|
164
|
+
const distList = result.DistributionList || result
|
|
165
|
+
const items = distList.Items
|
|
166
|
+
const summaryData = items?.DistributionSummary
|
|
167
|
+
|
|
168
|
+
if (summaryData) {
|
|
169
|
+
const summaries = Array.isArray(summaryData)
|
|
170
|
+
? summaryData
|
|
171
|
+
: [summaryData]
|
|
172
|
+
|
|
173
|
+
distributions.push(...summaries.map((item: any) => ({
|
|
174
|
+
Id: item.Id,
|
|
175
|
+
ARN: item.ARN,
|
|
176
|
+
Status: item.Status,
|
|
177
|
+
DomainName: item.DomainName,
|
|
178
|
+
Aliases: item.Aliases || undefined,
|
|
179
|
+
Enabled: item.Enabled === 'true' || item.Enabled === true,
|
|
180
|
+
})))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return distributions
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get distribution by ID
|
|
188
|
+
*/
|
|
189
|
+
async getDistribution(distributionId: string): Promise<Distribution> {
|
|
190
|
+
const result = await this.client.request({
|
|
191
|
+
service: 'cloudfront',
|
|
192
|
+
region: 'us-east-1',
|
|
193
|
+
method: 'GET',
|
|
194
|
+
path: `/2020-05-31/distribution/${distributionId}`,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const dist = result.Distribution || result
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
Id: dist.Id,
|
|
201
|
+
ARN: dist.ARN,
|
|
202
|
+
Status: dist.Status,
|
|
203
|
+
DomainName: dist.DomainName,
|
|
204
|
+
Aliases: dist.DistributionConfig?.Aliases?.Items || dist.Aliases?.Items || [],
|
|
205
|
+
Enabled: dist.DistributionConfig?.Enabled === 'true' || dist.DistributionConfig?.Enabled === true,
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get distribution configuration (full config including origins and cache behaviors)
|
|
211
|
+
*/
|
|
212
|
+
async getDistributionConfig(distributionId: string): Promise<{
|
|
213
|
+
ETag: string
|
|
214
|
+
DistributionConfig: {
|
|
215
|
+
Origins: {
|
|
216
|
+
Quantity: number
|
|
217
|
+
Items: any
|
|
218
|
+
}
|
|
219
|
+
DefaultCacheBehavior: {
|
|
220
|
+
TargetOriginId: string
|
|
221
|
+
ViewerProtocolPolicy: string
|
|
222
|
+
AllowedMethods?: { Quantity: number, Items: string[] }
|
|
223
|
+
CachedMethods?: { Quantity: number, Items: string[] }
|
|
224
|
+
ForwardedValues?: any
|
|
225
|
+
TrustedSigners?: any
|
|
226
|
+
MinTTL?: number
|
|
227
|
+
DefaultTTL?: number
|
|
228
|
+
MaxTTL?: number
|
|
229
|
+
}
|
|
230
|
+
CacheBehaviors?: {
|
|
231
|
+
Quantity: number
|
|
232
|
+
Items: Array<{
|
|
233
|
+
PathPattern: string
|
|
234
|
+
TargetOriginId: string
|
|
235
|
+
ViewerProtocolPolicy: string
|
|
236
|
+
AllowedMethods?: { Quantity: number, Items: string[] }
|
|
237
|
+
CachedMethods?: { Quantity: number, Items: string[] }
|
|
238
|
+
ForwardedValues?: any
|
|
239
|
+
MinTTL?: number
|
|
240
|
+
DefaultTTL?: number
|
|
241
|
+
MaxTTL?: number
|
|
242
|
+
}>
|
|
243
|
+
}
|
|
244
|
+
Aliases?: { Quantity: number, Items: string[] }
|
|
245
|
+
Comment?: string
|
|
246
|
+
Enabled: boolean
|
|
247
|
+
}
|
|
248
|
+
}> {
|
|
249
|
+
const result = await this.client.request({
|
|
250
|
+
service: 'cloudfront',
|
|
251
|
+
region: 'us-east-1',
|
|
252
|
+
method: 'GET',
|
|
253
|
+
path: `/2020-05-31/distribution/${distributionId}/config`,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
ETag: result.ETag || '',
|
|
258
|
+
DistributionConfig: result.DistributionConfig || result,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Invalidate all files
|
|
264
|
+
*/
|
|
265
|
+
async invalidateAll(distributionId: string): Promise<{
|
|
266
|
+
Id: string
|
|
267
|
+
Status: string
|
|
268
|
+
CreateTime: string
|
|
269
|
+
}> {
|
|
270
|
+
return this.createInvalidation({
|
|
271
|
+
distributionId,
|
|
272
|
+
paths: ['/*'],
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Invalidate specific paths
|
|
278
|
+
*/
|
|
279
|
+
async invalidatePaths(distributionId: string, paths: string[]): Promise<{
|
|
280
|
+
Id: string
|
|
281
|
+
Status: string
|
|
282
|
+
CreateTime: string
|
|
283
|
+
}> {
|
|
284
|
+
// Ensure paths start with /
|
|
285
|
+
const formattedPaths = paths.map(path => path.startsWith('/') ? path : `/${path}`)
|
|
286
|
+
|
|
287
|
+
return this.createInvalidation({
|
|
288
|
+
distributionId,
|
|
289
|
+
paths: formattedPaths,
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Invalidate by pattern
|
|
295
|
+
*/
|
|
296
|
+
async invalidatePattern(distributionId: string, pattern: string): Promise<{
|
|
297
|
+
Id: string
|
|
298
|
+
Status: string
|
|
299
|
+
CreateTime: string
|
|
300
|
+
}> {
|
|
301
|
+
// CloudFront supports wildcards like /images/* or /css/*
|
|
302
|
+
const path = pattern.startsWith('/') ? pattern : `/${pattern}`
|
|
303
|
+
|
|
304
|
+
return this.createInvalidation({
|
|
305
|
+
distributionId,
|
|
306
|
+
paths: [path],
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Invalidate after deployment
|
|
312
|
+
* Useful for CI/CD pipelines
|
|
313
|
+
*/
|
|
314
|
+
async invalidateAfterDeployment(options: {
|
|
315
|
+
distributionId: string
|
|
316
|
+
changedPaths?: string[]
|
|
317
|
+
invalidateAll?: boolean
|
|
318
|
+
wait?: boolean
|
|
319
|
+
}): Promise<{
|
|
320
|
+
invalidationId: string
|
|
321
|
+
status: string
|
|
322
|
+
}> {
|
|
323
|
+
const { distributionId, changedPaths, invalidateAll = false, wait = false } = options
|
|
324
|
+
|
|
325
|
+
let result
|
|
326
|
+
|
|
327
|
+
if (invalidateAll || !changedPaths || changedPaths.length === 0) {
|
|
328
|
+
result = await this.invalidateAll(distributionId)
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
result = await this.invalidatePaths(distributionId, changedPaths)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (wait) {
|
|
335
|
+
await this.waitForInvalidation(distributionId, result.Id)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
invalidationId: result.Id,
|
|
340
|
+
status: result.Status,
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Find distribution by domain name or alias
|
|
346
|
+
*/
|
|
347
|
+
async findDistributionByDomain(domain: string): Promise<Distribution | null> {
|
|
348
|
+
const distributions = await this.listDistributions()
|
|
349
|
+
|
|
350
|
+
// Check both CloudFront domain and aliases
|
|
351
|
+
const found = distributions.find((dist) => {
|
|
352
|
+
if (dist.DomainName === domain) {
|
|
353
|
+
return true
|
|
354
|
+
}
|
|
355
|
+
if (dist.Aliases?.Items && dist.Aliases.Items.includes(domain)) {
|
|
356
|
+
return true
|
|
357
|
+
}
|
|
358
|
+
return false
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
return found || null
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Batch invalidate multiple distributions
|
|
366
|
+
* Useful for multi-region or blue/green deployments
|
|
367
|
+
*/
|
|
368
|
+
async batchInvalidate(distributionIds: string[], paths: string[] = ['/*']): Promise<Array<{
|
|
369
|
+
distributionId: string
|
|
370
|
+
invalidationId: string
|
|
371
|
+
status: string
|
|
372
|
+
}>> {
|
|
373
|
+
const results = await Promise.all(
|
|
374
|
+
distributionIds.map(async (distributionId) => {
|
|
375
|
+
const result = await this.createInvalidation({
|
|
376
|
+
distributionId,
|
|
377
|
+
paths,
|
|
378
|
+
})
|
|
379
|
+
return {
|
|
380
|
+
distributionId,
|
|
381
|
+
invalidationId: result.Id,
|
|
382
|
+
status: result.Status,
|
|
383
|
+
}
|
|
384
|
+
}),
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
return results
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Update custom error responses for a distribution
|
|
392
|
+
* Use this to configure how CloudFront handles 4xx/5xx errors from origins
|
|
393
|
+
*/
|
|
394
|
+
async updateCustomErrorResponses(options: {
|
|
395
|
+
distributionId: string
|
|
396
|
+
customErrorResponses: Array<{
|
|
397
|
+
errorCode: number
|
|
398
|
+
responsePagePath?: string
|
|
399
|
+
responseCode?: number
|
|
400
|
+
errorCachingMinTTL?: number
|
|
401
|
+
}>
|
|
402
|
+
}): Promise<{
|
|
403
|
+
Distribution: Distribution
|
|
404
|
+
ETag: string
|
|
405
|
+
}> {
|
|
406
|
+
const { distributionId, customErrorResponses } = options
|
|
407
|
+
|
|
408
|
+
// First, get the current config with ETag
|
|
409
|
+
const getResult = await this.client.request({
|
|
410
|
+
service: 'cloudfront',
|
|
411
|
+
region: 'us-east-1',
|
|
412
|
+
method: 'GET',
|
|
413
|
+
path: `/2020-05-31/distribution/${distributionId}/config`,
|
|
414
|
+
returnHeaders: true,
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const etag = getResult.headers?.etag || getResult.headers?.ETag || ''
|
|
418
|
+
const currentConfig = getResult.body?.DistributionConfig || getResult.DistributionConfig
|
|
419
|
+
|
|
420
|
+
if (!currentConfig) {
|
|
421
|
+
throw new Error('Failed to get current distribution config')
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Update custom error responses
|
|
425
|
+
if (customErrorResponses.length === 0) {
|
|
426
|
+
currentConfig.CustomErrorResponses = {
|
|
427
|
+
Quantity: 0,
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
currentConfig.CustomErrorResponses = {
|
|
432
|
+
Quantity: customErrorResponses.length,
|
|
433
|
+
Items: {
|
|
434
|
+
CustomErrorResponse: customErrorResponses.map(err => ({
|
|
435
|
+
ErrorCode: err.errorCode,
|
|
436
|
+
...(err.responsePagePath && { ResponsePagePath: err.responsePagePath }),
|
|
437
|
+
...(err.responseCode && { ResponseCode: err.responseCode }),
|
|
438
|
+
...(err.errorCachingMinTTL !== undefined && { ErrorCachingMinTTL: err.errorCachingMinTTL }),
|
|
439
|
+
})),
|
|
440
|
+
},
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Build the XML for the update request
|
|
445
|
+
const configXml = this.buildDistributionConfigXml(currentConfig)
|
|
446
|
+
|
|
447
|
+
// Update the distribution
|
|
448
|
+
const result = await this.client.request({
|
|
449
|
+
service: 'cloudfront',
|
|
450
|
+
region: 'us-east-1',
|
|
451
|
+
method: 'PUT',
|
|
452
|
+
path: `/2020-05-31/distribution/${distributionId}/config`,
|
|
453
|
+
body: configXml,
|
|
454
|
+
headers: {
|
|
455
|
+
'Content-Type': 'application/xml',
|
|
456
|
+
'If-Match': etag,
|
|
457
|
+
},
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
const dist = result.Distribution || result
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
Distribution: {
|
|
464
|
+
Id: dist.Id,
|
|
465
|
+
ARN: dist.ARN,
|
|
466
|
+
Status: dist.Status,
|
|
467
|
+
DomainName: dist.DomainName,
|
|
468
|
+
Aliases: dist.DistributionConfig?.Aliases?.Items || [],
|
|
469
|
+
Enabled: dist.DistributionConfig?.Enabled === 'true' || dist.DistributionConfig?.Enabled === true,
|
|
470
|
+
},
|
|
471
|
+
ETag: result.ETag || '',
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Remove all custom error responses from a distribution
|
|
477
|
+
* This will make CloudFront return actual 4xx/5xx errors instead of custom pages
|
|
478
|
+
*/
|
|
479
|
+
async removeCustomErrorResponses(distributionId: string): Promise<{
|
|
480
|
+
Distribution: Distribution
|
|
481
|
+
ETag: string
|
|
482
|
+
}> {
|
|
483
|
+
return this.updateCustomErrorResponses({
|
|
484
|
+
distributionId,
|
|
485
|
+
customErrorResponses: [],
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Update distribution configuration
|
|
491
|
+
* This method updates the CloudFront distribution with new settings like aliases and certificates
|
|
492
|
+
*/
|
|
493
|
+
async updateDistribution(options: {
|
|
494
|
+
distributionId: string
|
|
495
|
+
aliases?: string[]
|
|
496
|
+
certificateArn?: string
|
|
497
|
+
comment?: string
|
|
498
|
+
}): Promise<{
|
|
499
|
+
Distribution: Distribution
|
|
500
|
+
ETag: string
|
|
501
|
+
}> {
|
|
502
|
+
const { distributionId, aliases, certificateArn, comment } = options
|
|
503
|
+
|
|
504
|
+
// First, get the current config with ETag
|
|
505
|
+
const getResult = await this.client.request({
|
|
506
|
+
service: 'cloudfront',
|
|
507
|
+
region: 'us-east-1',
|
|
508
|
+
method: 'GET',
|
|
509
|
+
path: `/2020-05-31/distribution/${distributionId}/config`,
|
|
510
|
+
returnHeaders: true,
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
const etag = getResult.headers?.etag || getResult.headers?.ETag || ''
|
|
514
|
+
const currentConfig = getResult.body?.DistributionConfig || getResult.DistributionConfig
|
|
515
|
+
|
|
516
|
+
if (!currentConfig) {
|
|
517
|
+
throw new Error('Failed to get current distribution config')
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Update the config with new values
|
|
521
|
+
if (aliases && aliases.length > 0) {
|
|
522
|
+
currentConfig.Aliases = {
|
|
523
|
+
Quantity: aliases.length,
|
|
524
|
+
Items: { Item: aliases },
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (certificateArn) {
|
|
529
|
+
currentConfig.ViewerCertificate = {
|
|
530
|
+
ACMCertificateArn: certificateArn,
|
|
531
|
+
SSLSupportMethod: 'sni-only',
|
|
532
|
+
MinimumProtocolVersion: 'TLSv1.2_2021',
|
|
533
|
+
CertificateSource: 'acm',
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (comment) {
|
|
538
|
+
currentConfig.Comment = comment
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Build the XML for the update request
|
|
542
|
+
const configXml = this.buildDistributionConfigXml(currentConfig)
|
|
543
|
+
|
|
544
|
+
// Update the distribution
|
|
545
|
+
const result = await this.client.request({
|
|
546
|
+
service: 'cloudfront',
|
|
547
|
+
region: 'us-east-1',
|
|
548
|
+
method: 'PUT',
|
|
549
|
+
path: `/2020-05-31/distribution/${distributionId}/config`,
|
|
550
|
+
body: configXml,
|
|
551
|
+
headers: {
|
|
552
|
+
'Content-Type': 'application/xml',
|
|
553
|
+
'If-Match': etag,
|
|
554
|
+
},
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
const dist = result.Distribution || result
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
Distribution: {
|
|
561
|
+
Id: dist.Id,
|
|
562
|
+
ARN: dist.ARN,
|
|
563
|
+
Status: dist.Status,
|
|
564
|
+
DomainName: dist.DomainName,
|
|
565
|
+
Aliases: aliases ? { Quantity: aliases.length, Items: aliases } : { Quantity: 0, Items: [] },
|
|
566
|
+
Enabled: dist.DistributionConfig?.Enabled === 'true' || dist.DistributionConfig?.Enabled === true,
|
|
567
|
+
},
|
|
568
|
+
ETag: result.ETag || '',
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Helper to build XML from distribution config object
|
|
574
|
+
* CloudFront requires specific XML structures - this method handles the complex nesting
|
|
575
|
+
*/
|
|
576
|
+
private buildDistributionConfigXml(config: any): string {
|
|
577
|
+
const escapeXml = (str: string): string => {
|
|
578
|
+
return str.replace(/&/g, '&')
|
|
579
|
+
.replace(/</g, '<')
|
|
580
|
+
.replace(/>/g, '>')
|
|
581
|
+
.replace(/"/g, '"')
|
|
582
|
+
.replace(/'/g, ''')
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Map of parent element names to their child element names for array items
|
|
586
|
+
const arrayChildNames: Record<string, string> = {
|
|
587
|
+
Items: '', // Will be determined by context
|
|
588
|
+
Methods: 'Method',
|
|
589
|
+
Headers: 'Name',
|
|
590
|
+
Cookies: 'Name',
|
|
591
|
+
QueryStringCacheKeys: 'Name',
|
|
592
|
+
TrustedKeyGroups: 'KeyGroup',
|
|
593
|
+
TrustedSigners: 'AwsAccountNumber',
|
|
594
|
+
LambdaFunctionAssociations: 'LambdaFunctionAssociation',
|
|
595
|
+
FunctionAssociations: 'FunctionAssociation',
|
|
596
|
+
CacheBehaviors: 'CacheBehavior',
|
|
597
|
+
CustomErrorResponses: 'CustomErrorResponse',
|
|
598
|
+
GeoRestriction: 'Location',
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Elements inside Items that have specific child names
|
|
602
|
+
const itemsChildNames: Record<string, string> = {
|
|
603
|
+
Origins: 'Origin',
|
|
604
|
+
Aliases: 'CNAME',
|
|
605
|
+
AllowedMethods: 'Method',
|
|
606
|
+
CachedMethods: 'Method',
|
|
607
|
+
CustomErrorResponses: 'CustomErrorResponse',
|
|
608
|
+
CacheBehaviors: 'CacheBehavior',
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const buildXmlElement = (name: string, value: any, indent: string = '', parentContext: string = ''): string => {
|
|
612
|
+
if (value === null || value === undefined) {
|
|
613
|
+
return ''
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Skip XML metadata attributes
|
|
617
|
+
if (name.startsWith('@_') || name === '?xml') {
|
|
618
|
+
return ''
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (typeof value === 'boolean') {
|
|
622
|
+
return `${indent}<${name}>${value}</${name}>\n`
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (typeof value === 'number' || typeof value === 'string') {
|
|
626
|
+
return `${indent}<${name}>${escapeXml(String(value))}</${name}>\n`
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (Array.isArray(value)) {
|
|
630
|
+
// For arrays, we need to output each item with the appropriate element name
|
|
631
|
+
const childName = arrayChildNames[name] || name.replace(/s$/, '')
|
|
632
|
+
return value.map(item => buildXmlElement(childName, item, indent, name)).join('')
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (typeof value === 'object') {
|
|
636
|
+
// Handle Items specially - they contain the actual array items
|
|
637
|
+
if (name === 'Items') {
|
|
638
|
+
// Figure out what type of items these are based on parent context
|
|
639
|
+
const childElementName = itemsChildNames[parentContext] || ''
|
|
640
|
+
|
|
641
|
+
// Check if Items has named children (like CNAME, Origin, etc.)
|
|
642
|
+
const keys = Object.keys(value).filter(k => !k.startsWith('@_'))
|
|
643
|
+
|
|
644
|
+
if (keys.length === 1 && !Array.isArray(value[keys[0]])) {
|
|
645
|
+
// Single named child that's not an array - could be a single item
|
|
646
|
+
const childKey = keys[0]
|
|
647
|
+
const childValue = value[childKey]
|
|
648
|
+
if (typeof childValue === 'string') {
|
|
649
|
+
// Single item like {CNAME: "domain.com"}
|
|
650
|
+
return `${indent}<Items>\n${indent} <${childKey}>${escapeXml(childValue)}</${childKey}>\n${indent}</Items>\n`
|
|
651
|
+
}
|
|
652
|
+
else if (typeof childValue === 'object' && !Array.isArray(childValue)) {
|
|
653
|
+
// Single complex item like {Origin: {...}}
|
|
654
|
+
return `${indent}<Items>\n${buildXmlElement(childKey, childValue, indent + ' ', name)}${indent}</Items>\n`
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (keys.length === 1 && Array.isArray(value[keys[0]])) {
|
|
659
|
+
// Named array child like {CNAME: ["a.com", "b.com"]} or {Origin: [{...}, {...}]}
|
|
660
|
+
const childKey = keys[0]
|
|
661
|
+
const childArray = value[childKey]
|
|
662
|
+
let children = ''
|
|
663
|
+
for (const item of childArray) {
|
|
664
|
+
if (typeof item === 'string') {
|
|
665
|
+
children += `${indent} <${childKey}>${escapeXml(item)}</${childKey}>\n`
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
children += buildXmlElement(childKey, item, indent + ' ', name)
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return `${indent}<Items>\n${children}${indent}</Items>\n`
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Check if Items is an array directly passed in
|
|
675
|
+
if (Array.isArray(value)) {
|
|
676
|
+
let children = ''
|
|
677
|
+
const childName = childElementName || 'Item'
|
|
678
|
+
for (const item of value) {
|
|
679
|
+
if (typeof item === 'string') {
|
|
680
|
+
children += `${indent} <${childName}>${escapeXml(item)}</${childName}>\n`
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
children += buildXmlElement(childName, item, indent + ' ', name)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return `${indent}<Items>\n${children}${indent}</Items>\n`
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Fall through to regular object handling if none of the special cases match
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
let children = ''
|
|
693
|
+
for (const [key, val] of Object.entries(value)) {
|
|
694
|
+
if (!key.startsWith('@_')) {
|
|
695
|
+
children += buildXmlElement(key, val, indent + ' ', name)
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (children === '') {
|
|
700
|
+
return `${indent}<${name}/>\n`
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return `${indent}<${name}>\n${children}${indent}</${name}>\n`
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return ''
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">\n${Object.entries(config).filter(([k]) => !k.startsWith('@_')).map(([key, val]) => buildXmlElement(key, val, ' ', 'DistributionConfig')).join('')}</DistributionConfig>`
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Add aliases to a distribution
|
|
714
|
+
*/
|
|
715
|
+
async addAliases(distributionId: string, aliases: string[], certificateArn: string): Promise<{
|
|
716
|
+
Distribution: Distribution
|
|
717
|
+
ETag: string
|
|
718
|
+
}> {
|
|
719
|
+
return this.updateDistribution({
|
|
720
|
+
distributionId,
|
|
721
|
+
aliases,
|
|
722
|
+
certificateArn,
|
|
723
|
+
})
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Create a CloudFront Function
|
|
728
|
+
* CloudFront Functions are lightweight JavaScript functions for viewer request/response manipulation
|
|
729
|
+
*/
|
|
730
|
+
async createFunction(options: {
|
|
731
|
+
name: string
|
|
732
|
+
code: string
|
|
733
|
+
comment?: string
|
|
734
|
+
runtime?: 'cloudfront-js-1.0' | 'cloudfront-js-2.0'
|
|
735
|
+
}): Promise<{
|
|
736
|
+
FunctionARN: string
|
|
737
|
+
Name: string
|
|
738
|
+
Stage: string
|
|
739
|
+
ETag: string
|
|
740
|
+
}> {
|
|
741
|
+
const { name, code, comment = '', runtime = 'cloudfront-js-2.0' } = options
|
|
742
|
+
|
|
743
|
+
const functionXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
744
|
+
<CreateFunctionRequest xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
|
|
745
|
+
<Name>${name}</Name>
|
|
746
|
+
<FunctionConfig>
|
|
747
|
+
<Comment>${comment}</Comment>
|
|
748
|
+
<Runtime>${runtime}</Runtime>
|
|
749
|
+
</FunctionConfig>
|
|
750
|
+
<FunctionCode>${Buffer.from(code).toString('base64')}</FunctionCode>
|
|
751
|
+
</CreateFunctionRequest>`
|
|
752
|
+
|
|
753
|
+
const result = await this.client.request({
|
|
754
|
+
service: 'cloudfront',
|
|
755
|
+
region: 'us-east-1',
|
|
756
|
+
method: 'POST',
|
|
757
|
+
path: '/2020-05-31/function',
|
|
758
|
+
body: functionXml,
|
|
759
|
+
headers: {
|
|
760
|
+
'Content-Type': 'application/xml',
|
|
761
|
+
},
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
const func = result.FunctionSummary || result
|
|
765
|
+
|
|
766
|
+
return {
|
|
767
|
+
FunctionARN: func.FunctionMetadata?.FunctionARN || func.FunctionARN,
|
|
768
|
+
Name: func.Name || name,
|
|
769
|
+
Stage: func.FunctionMetadata?.Stage || 'DEVELOPMENT',
|
|
770
|
+
ETag: result.ETag || '',
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* List CloudFront Functions
|
|
776
|
+
*/
|
|
777
|
+
async listFunctions(): Promise<Array<{
|
|
778
|
+
Name: string
|
|
779
|
+
FunctionARN: string
|
|
780
|
+
Stage: string
|
|
781
|
+
CreatedTime: string
|
|
782
|
+
LastModifiedTime: string
|
|
783
|
+
}>> {
|
|
784
|
+
const result = await this.client.request({
|
|
785
|
+
service: 'cloudfront',
|
|
786
|
+
region: 'us-east-1',
|
|
787
|
+
method: 'GET',
|
|
788
|
+
path: '/2020-05-31/function',
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
const functions: Array<{
|
|
792
|
+
Name: string
|
|
793
|
+
FunctionARN: string
|
|
794
|
+
Stage: string
|
|
795
|
+
CreatedTime: string
|
|
796
|
+
LastModifiedTime: string
|
|
797
|
+
}> = []
|
|
798
|
+
|
|
799
|
+
const items = result.FunctionList?.Items?.FunctionSummary
|
|
800
|
+
if (items) {
|
|
801
|
+
const list = Array.isArray(items) ? items : [items]
|
|
802
|
+
for (const item of list) {
|
|
803
|
+
functions.push({
|
|
804
|
+
Name: item.Name,
|
|
805
|
+
FunctionARN: item.FunctionMetadata?.FunctionARN,
|
|
806
|
+
Stage: item.FunctionMetadata?.Stage,
|
|
807
|
+
CreatedTime: item.FunctionMetadata?.CreatedTime,
|
|
808
|
+
LastModifiedTime: item.FunctionMetadata?.LastModifiedTime,
|
|
809
|
+
})
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return functions
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Get a CloudFront Function
|
|
818
|
+
*/
|
|
819
|
+
async getFunction(name: string, stage: 'DEVELOPMENT' | 'LIVE' = 'LIVE'): Promise<{
|
|
820
|
+
FunctionARN: string
|
|
821
|
+
Name: string
|
|
822
|
+
Stage: string
|
|
823
|
+
ETag: string
|
|
824
|
+
FunctionCode?: string
|
|
825
|
+
} | null> {
|
|
826
|
+
try {
|
|
827
|
+
const result = await this.client.request({
|
|
828
|
+
service: 'cloudfront',
|
|
829
|
+
region: 'us-east-1',
|
|
830
|
+
method: 'GET',
|
|
831
|
+
path: `/2020-05-31/function/${name}`,
|
|
832
|
+
queryParams: { Stage: stage },
|
|
833
|
+
returnHeaders: true,
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
const func = result.body?.FunctionSummary || result.FunctionSummary || result.body || result
|
|
837
|
+
|
|
838
|
+
return {
|
|
839
|
+
FunctionARN: func.FunctionMetadata?.FunctionARN,
|
|
840
|
+
Name: func.Name || name,
|
|
841
|
+
Stage: func.FunctionMetadata?.Stage || stage,
|
|
842
|
+
ETag: result.headers?.etag || result.ETag || '',
|
|
843
|
+
FunctionCode: func.FunctionCode,
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
catch (err: any) {
|
|
847
|
+
if (err.message?.includes('404') || err.message?.includes('NoSuchFunctionExists')) {
|
|
848
|
+
return null
|
|
849
|
+
}
|
|
850
|
+
throw err
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Publish a CloudFront Function (move from DEVELOPMENT to LIVE stage)
|
|
856
|
+
* Can be called with just the name (will auto-fetch ETag) or with options object
|
|
857
|
+
*/
|
|
858
|
+
async publishFunction(nameOrOptions: string | { Name: string, IfMatch: string }, etag?: string): Promise<{
|
|
859
|
+
FunctionARN: string
|
|
860
|
+
Stage: string
|
|
861
|
+
FunctionSummary?: {
|
|
862
|
+
Name: string
|
|
863
|
+
FunctionMetadata: {
|
|
864
|
+
FunctionARN: string
|
|
865
|
+
Stage: string
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}> {
|
|
869
|
+
let name: string
|
|
870
|
+
let functionETag: string | undefined
|
|
871
|
+
|
|
872
|
+
if (typeof nameOrOptions === 'object') {
|
|
873
|
+
name = nameOrOptions.Name
|
|
874
|
+
functionETag = nameOrOptions.IfMatch
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
name = nameOrOptions
|
|
878
|
+
functionETag = etag
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Get the current ETag if not provided
|
|
882
|
+
if (!functionETag) {
|
|
883
|
+
const func = await this.getFunction(name, 'DEVELOPMENT')
|
|
884
|
+
if (!func) {
|
|
885
|
+
throw new Error(`Function ${name} not found`)
|
|
886
|
+
}
|
|
887
|
+
functionETag = func.ETag
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const result = await this.client.request({
|
|
891
|
+
service: 'cloudfront',
|
|
892
|
+
region: 'us-east-1',
|
|
893
|
+
method: 'POST',
|
|
894
|
+
path: `/2020-05-31/function/${name}/publish`,
|
|
895
|
+
headers: {
|
|
896
|
+
'If-Match': functionETag,
|
|
897
|
+
},
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
const func = result.FunctionSummary || result
|
|
901
|
+
|
|
902
|
+
return {
|
|
903
|
+
FunctionARN: func.FunctionMetadata?.FunctionARN,
|
|
904
|
+
Stage: func.FunctionMetadata?.Stage || 'LIVE',
|
|
905
|
+
FunctionSummary: func,
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Describe a CloudFront Function (get metadata including ETag)
|
|
911
|
+
*/
|
|
912
|
+
async describeFunction(options: { Name: string, Stage?: 'DEVELOPMENT' | 'LIVE' }): Promise<{
|
|
913
|
+
ETag: string
|
|
914
|
+
FunctionSummary: {
|
|
915
|
+
Name: string
|
|
916
|
+
Status: string
|
|
917
|
+
FunctionConfig: {
|
|
918
|
+
Comment: string
|
|
919
|
+
Runtime: string
|
|
920
|
+
}
|
|
921
|
+
FunctionMetadata: {
|
|
922
|
+
FunctionARN: string
|
|
923
|
+
Stage: string
|
|
924
|
+
CreatedTime: string
|
|
925
|
+
LastModifiedTime: string
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}> {
|
|
929
|
+
const { Name, Stage = 'DEVELOPMENT' } = options
|
|
930
|
+
|
|
931
|
+
const result = await this.client.request({
|
|
932
|
+
service: 'cloudfront',
|
|
933
|
+
region: 'us-east-1',
|
|
934
|
+
method: 'GET',
|
|
935
|
+
path: `/2020-05-31/function/${Name}/describe`,
|
|
936
|
+
queryParams: { Stage },
|
|
937
|
+
returnHeaders: true,
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
return {
|
|
941
|
+
ETag: result.headers?.etag || result.ETag || '',
|
|
942
|
+
FunctionSummary: result.body?.FunctionSummary || result.FunctionSummary || result.body || result,
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Update a CloudFront Function
|
|
948
|
+
*/
|
|
949
|
+
async updateFunction(options: {
|
|
950
|
+
Name: string
|
|
951
|
+
FunctionCode: string
|
|
952
|
+
FunctionConfig: {
|
|
953
|
+
Comment: string
|
|
954
|
+
Runtime: 'cloudfront-js-1.0' | 'cloudfront-js-2.0'
|
|
955
|
+
}
|
|
956
|
+
IfMatch: string
|
|
957
|
+
}): Promise<{
|
|
958
|
+
ETag: string
|
|
959
|
+
FunctionSummary: {
|
|
960
|
+
Name: string
|
|
961
|
+
FunctionMetadata: {
|
|
962
|
+
FunctionARN: string
|
|
963
|
+
Stage: string
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}> {
|
|
967
|
+
const { Name, FunctionCode, FunctionConfig, IfMatch } = options
|
|
968
|
+
|
|
969
|
+
const functionXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
970
|
+
<UpdateFunctionRequest xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
|
|
971
|
+
<FunctionConfig>
|
|
972
|
+
<Comment>${FunctionConfig.Comment}</Comment>
|
|
973
|
+
<Runtime>${FunctionConfig.Runtime}</Runtime>
|
|
974
|
+
</FunctionConfig>
|
|
975
|
+
<FunctionCode>${Buffer.from(FunctionCode).toString('base64')}</FunctionCode>
|
|
976
|
+
</UpdateFunctionRequest>`
|
|
977
|
+
|
|
978
|
+
const result = await this.client.request({
|
|
979
|
+
service: 'cloudfront',
|
|
980
|
+
region: 'us-east-1',
|
|
981
|
+
method: 'PUT',
|
|
982
|
+
path: `/2020-05-31/function/${Name}`,
|
|
983
|
+
body: functionXml,
|
|
984
|
+
headers: {
|
|
985
|
+
'Content-Type': 'application/xml',
|
|
986
|
+
'If-Match': IfMatch,
|
|
987
|
+
},
|
|
988
|
+
returnHeaders: true,
|
|
989
|
+
})
|
|
990
|
+
|
|
991
|
+
return {
|
|
992
|
+
ETag: result.headers?.etag || result.ETag || '',
|
|
993
|
+
FunctionSummary: result.body?.FunctionSummary || result.FunctionSummary || result.body || result,
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Delete a CloudFront Function
|
|
999
|
+
*/
|
|
1000
|
+
async deleteFunction(name: string, etag?: string): Promise<void> {
|
|
1001
|
+
// Get the current ETag if not provided
|
|
1002
|
+
let functionETag = etag
|
|
1003
|
+
if (!functionETag) {
|
|
1004
|
+
const func = await this.getFunction(name, 'DEVELOPMENT')
|
|
1005
|
+
if (!func) {
|
|
1006
|
+
// Function doesn't exist, nothing to delete
|
|
1007
|
+
return
|
|
1008
|
+
}
|
|
1009
|
+
functionETag = func.ETag
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
await this.client.request({
|
|
1013
|
+
service: 'cloudfront',
|
|
1014
|
+
region: 'us-east-1',
|
|
1015
|
+
method: 'DELETE',
|
|
1016
|
+
path: `/2020-05-31/function/${name}`,
|
|
1017
|
+
headers: {
|
|
1018
|
+
'If-Match': functionETag,
|
|
1019
|
+
},
|
|
1020
|
+
})
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Create a standard index.html rewrite function for S3 static sites
|
|
1025
|
+
* This function rewrites directory requests to index.html
|
|
1026
|
+
*/
|
|
1027
|
+
async createIndexRewriteFunction(name: string): Promise<{
|
|
1028
|
+
FunctionARN: string
|
|
1029
|
+
Name: string
|
|
1030
|
+
Stage: string
|
|
1031
|
+
ETag: string
|
|
1032
|
+
}> {
|
|
1033
|
+
const code = `function handler(event) {
|
|
1034
|
+
var request = event.request;
|
|
1035
|
+
var uri = request.uri;
|
|
1036
|
+
|
|
1037
|
+
// Check if the request is for a directory (ends with /)
|
|
1038
|
+
if (uri.endsWith('/')) {
|
|
1039
|
+
request.uri += 'index.html';
|
|
1040
|
+
}
|
|
1041
|
+
// Check if the request doesn't have a file extension
|
|
1042
|
+
else if (!uri.includes('.')) {
|
|
1043
|
+
// Add trailing slash to redirect to directory
|
|
1044
|
+
request.uri += '/index.html';
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
return request;
|
|
1048
|
+
}`
|
|
1049
|
+
|
|
1050
|
+
return this.createFunction({
|
|
1051
|
+
name,
|
|
1052
|
+
code,
|
|
1053
|
+
comment: 'Rewrite directory requests to index.html for S3 static sites',
|
|
1054
|
+
runtime: 'cloudfront-js-2.0',
|
|
1055
|
+
})
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Get origin access control configurations
|
|
1060
|
+
*/
|
|
1061
|
+
async listOriginAccessControls(): Promise<Array<{
|
|
1062
|
+
Id: string
|
|
1063
|
+
Name: string
|
|
1064
|
+
Description?: string
|
|
1065
|
+
SigningProtocol: string
|
|
1066
|
+
SigningBehavior: string
|
|
1067
|
+
OriginAccessControlOriginType: string
|
|
1068
|
+
}>> {
|
|
1069
|
+
const result = await this.client.request({
|
|
1070
|
+
service: 'cloudfront',
|
|
1071
|
+
region: 'us-east-1',
|
|
1072
|
+
method: 'GET',
|
|
1073
|
+
path: '/2020-05-31/origin-access-control',
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
const items: any[] = []
|
|
1077
|
+
|
|
1078
|
+
if (result.OriginAccessControlList?.Items?.OriginAccessControlSummary) {
|
|
1079
|
+
const summaries = Array.isArray(result.OriginAccessControlList.Items.OriginAccessControlSummary)
|
|
1080
|
+
? result.OriginAccessControlList.Items.OriginAccessControlSummary
|
|
1081
|
+
: [result.OriginAccessControlList.Items.OriginAccessControlSummary]
|
|
1082
|
+
|
|
1083
|
+
items.push(...summaries.map((item: any) => ({
|
|
1084
|
+
Id: item.Id,
|
|
1085
|
+
Name: item.Name,
|
|
1086
|
+
Description: item.Description,
|
|
1087
|
+
SigningProtocol: item.SigningProtocol,
|
|
1088
|
+
SigningBehavior: item.SigningBehavior,
|
|
1089
|
+
OriginAccessControlOriginType: item.OriginAccessControlOriginType,
|
|
1090
|
+
})))
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return items
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Create an Origin Access Control for S3
|
|
1098
|
+
*/
|
|
1099
|
+
async createOriginAccessControl(options: {
|
|
1100
|
+
name: string
|
|
1101
|
+
description?: string
|
|
1102
|
+
signingProtocol?: 'sigv4'
|
|
1103
|
+
signingBehavior?: 'always' | 'never' | 'no-override'
|
|
1104
|
+
originType?: 's3'
|
|
1105
|
+
}): Promise<{
|
|
1106
|
+
Id: string
|
|
1107
|
+
Name: string
|
|
1108
|
+
Description: string
|
|
1109
|
+
SigningProtocol: string
|
|
1110
|
+
SigningBehavior: string
|
|
1111
|
+
OriginAccessControlOriginType: string
|
|
1112
|
+
ETag: string
|
|
1113
|
+
}> {
|
|
1114
|
+
const {
|
|
1115
|
+
name,
|
|
1116
|
+
description = `OAC for ${name}`,
|
|
1117
|
+
signingProtocol = 'sigv4',
|
|
1118
|
+
signingBehavior = 'always',
|
|
1119
|
+
originType = 's3',
|
|
1120
|
+
} = options
|
|
1121
|
+
|
|
1122
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1123
|
+
<OriginAccessControlConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
|
|
1124
|
+
<Name>${name}</Name>
|
|
1125
|
+
<Description>${description}</Description>
|
|
1126
|
+
<SigningProtocol>${signingProtocol}</SigningProtocol>
|
|
1127
|
+
<SigningBehavior>${signingBehavior}</SigningBehavior>
|
|
1128
|
+
<OriginAccessControlOriginType>${originType}</OriginAccessControlOriginType>
|
|
1129
|
+
</OriginAccessControlConfig>`
|
|
1130
|
+
|
|
1131
|
+
const result = await this.client.request({
|
|
1132
|
+
service: 'cloudfront',
|
|
1133
|
+
region: 'us-east-1',
|
|
1134
|
+
method: 'POST',
|
|
1135
|
+
path: '/2020-05-31/origin-access-control',
|
|
1136
|
+
body,
|
|
1137
|
+
headers: {
|
|
1138
|
+
'Content-Type': 'application/xml',
|
|
1139
|
+
},
|
|
1140
|
+
returnHeaders: true,
|
|
1141
|
+
})
|
|
1142
|
+
|
|
1143
|
+
const oac = result.body?.OriginAccessControl || result.OriginAccessControl || result.body || result
|
|
1144
|
+
|
|
1145
|
+
return {
|
|
1146
|
+
Id: oac.Id,
|
|
1147
|
+
Name: oac.OriginAccessControlConfig?.Name || name,
|
|
1148
|
+
Description: oac.OriginAccessControlConfig?.Description || description,
|
|
1149
|
+
SigningProtocol: oac.OriginAccessControlConfig?.SigningProtocol || signingProtocol,
|
|
1150
|
+
SigningBehavior: oac.OriginAccessControlConfig?.SigningBehavior || signingBehavior,
|
|
1151
|
+
OriginAccessControlOriginType: oac.OriginAccessControlConfig?.OriginAccessControlOriginType || originType,
|
|
1152
|
+
ETag: result.headers?.etag || result.ETag || '',
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Find or create an Origin Access Control
|
|
1158
|
+
*/
|
|
1159
|
+
async findOrCreateOriginAccessControl(name: string): Promise<{
|
|
1160
|
+
Id: string
|
|
1161
|
+
Name: string
|
|
1162
|
+
isNew: boolean
|
|
1163
|
+
}> {
|
|
1164
|
+
const oacs = await this.listOriginAccessControls()
|
|
1165
|
+
const existing = oacs.find(oac => oac.Name === name)
|
|
1166
|
+
|
|
1167
|
+
if (existing) {
|
|
1168
|
+
return { Id: existing.Id, Name: existing.Name, isNew: false }
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const created = await this.createOriginAccessControl({ name })
|
|
1172
|
+
return { Id: created.Id, Name: created.Name, isNew: true }
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* Create a CloudFront distribution for a static S3 website
|
|
1177
|
+
*/
|
|
1178
|
+
async createDistributionForS3(options: {
|
|
1179
|
+
bucketName: string
|
|
1180
|
+
bucketRegion: string
|
|
1181
|
+
originAccessControlId: string
|
|
1182
|
+
aliases?: string[]
|
|
1183
|
+
certificateArn?: string
|
|
1184
|
+
defaultRootObject?: string
|
|
1185
|
+
comment?: string
|
|
1186
|
+
priceClass?: 'PriceClass_100' | 'PriceClass_200' | 'PriceClass_All'
|
|
1187
|
+
enabled?: boolean
|
|
1188
|
+
}): Promise<{
|
|
1189
|
+
Id: string
|
|
1190
|
+
ARN: string
|
|
1191
|
+
DomainName: string
|
|
1192
|
+
Status: string
|
|
1193
|
+
ETag: string
|
|
1194
|
+
}> {
|
|
1195
|
+
const {
|
|
1196
|
+
bucketName,
|
|
1197
|
+
bucketRegion,
|
|
1198
|
+
originAccessControlId,
|
|
1199
|
+
aliases = [],
|
|
1200
|
+
certificateArn,
|
|
1201
|
+
defaultRootObject = 'index.html',
|
|
1202
|
+
comment = `Distribution for ${bucketName}`,
|
|
1203
|
+
priceClass = 'PriceClass_100',
|
|
1204
|
+
enabled = true,
|
|
1205
|
+
} = options
|
|
1206
|
+
|
|
1207
|
+
const originId = `S3-${bucketName}`
|
|
1208
|
+
const s3DomainName = `${bucketName}.s3.${bucketRegion}.amazonaws.com`
|
|
1209
|
+
const callerReference = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
1210
|
+
|
|
1211
|
+
// Build aliases XML
|
|
1212
|
+
let aliasesXml = '<Aliases><Quantity>0</Quantity></Aliases>'
|
|
1213
|
+
if (aliases.length > 0) {
|
|
1214
|
+
aliasesXml = `<Aliases>
|
|
1215
|
+
<Quantity>${aliases.length}</Quantity>
|
|
1216
|
+
<Items>
|
|
1217
|
+
${aliases.map(a => `<CNAME>${a}</CNAME>`).join('\n ')}
|
|
1218
|
+
</Items>
|
|
1219
|
+
</Aliases>`
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Build viewer certificate XML
|
|
1223
|
+
let viewerCertificateXml = `<ViewerCertificate>
|
|
1224
|
+
<CloudFrontDefaultCertificate>true</CloudFrontDefaultCertificate>
|
|
1225
|
+
</ViewerCertificate>`
|
|
1226
|
+
|
|
1227
|
+
if (certificateArn && aliases.length > 0) {
|
|
1228
|
+
viewerCertificateXml = `<ViewerCertificate>
|
|
1229
|
+
<ACMCertificateArn>${certificateArn}</ACMCertificateArn>
|
|
1230
|
+
<SSLSupportMethod>sni-only</SSLSupportMethod>
|
|
1231
|
+
<MinimumProtocolVersion>TLSv1.2_2021</MinimumProtocolVersion>
|
|
1232
|
+
<CertificateSource>acm</CertificateSource>
|
|
1233
|
+
</ViewerCertificate>`
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1237
|
+
<DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
|
|
1238
|
+
<CallerReference>${callerReference}</CallerReference>
|
|
1239
|
+
<Comment>${comment}</Comment>
|
|
1240
|
+
<DefaultRootObject>${defaultRootObject}</DefaultRootObject>
|
|
1241
|
+
<Origins>
|
|
1242
|
+
<Quantity>1</Quantity>
|
|
1243
|
+
<Items>
|
|
1244
|
+
<Origin>
|
|
1245
|
+
<Id>${originId}</Id>
|
|
1246
|
+
<DomainName>${s3DomainName}</DomainName>
|
|
1247
|
+
<OriginPath></OriginPath>
|
|
1248
|
+
<S3OriginConfig>
|
|
1249
|
+
<OriginAccessIdentity></OriginAccessIdentity>
|
|
1250
|
+
</S3OriginConfig>
|
|
1251
|
+
<OriginAccessControlId>${originAccessControlId}</OriginAccessControlId>
|
|
1252
|
+
</Origin>
|
|
1253
|
+
</Items>
|
|
1254
|
+
</Origins>
|
|
1255
|
+
<DefaultCacheBehavior>
|
|
1256
|
+
<TargetOriginId>${originId}</TargetOriginId>
|
|
1257
|
+
<ViewerProtocolPolicy>redirect-to-https</ViewerProtocolPolicy>
|
|
1258
|
+
<AllowedMethods>
|
|
1259
|
+
<Quantity>2</Quantity>
|
|
1260
|
+
<Items>
|
|
1261
|
+
<Method>GET</Method>
|
|
1262
|
+
<Method>HEAD</Method>
|
|
1263
|
+
</Items>
|
|
1264
|
+
<CachedMethods>
|
|
1265
|
+
<Quantity>2</Quantity>
|
|
1266
|
+
<Items>
|
|
1267
|
+
<Method>GET</Method>
|
|
1268
|
+
<Method>HEAD</Method>
|
|
1269
|
+
</Items>
|
|
1270
|
+
</CachedMethods>
|
|
1271
|
+
</AllowedMethods>
|
|
1272
|
+
<Compress>true</Compress>
|
|
1273
|
+
<CachePolicyId>658327ea-f89d-4fab-a63d-7e88639e58f6</CachePolicyId>
|
|
1274
|
+
</DefaultCacheBehavior>
|
|
1275
|
+
${aliasesXml}
|
|
1276
|
+
${viewerCertificateXml}
|
|
1277
|
+
<PriceClass>${priceClass}</PriceClass>
|
|
1278
|
+
<Enabled>${enabled}</Enabled>
|
|
1279
|
+
<HttpVersion>http2and3</HttpVersion>
|
|
1280
|
+
<IsIPV6Enabled>true</IsIPV6Enabled>
|
|
1281
|
+
<CustomErrorResponses>
|
|
1282
|
+
<Quantity>1</Quantity>
|
|
1283
|
+
<Items>
|
|
1284
|
+
<CustomErrorResponse>
|
|
1285
|
+
<ErrorCode>403</ErrorCode>
|
|
1286
|
+
<ResponsePagePath>/index.html</ResponsePagePath>
|
|
1287
|
+
<ResponseCode>200</ResponseCode>
|
|
1288
|
+
<ErrorCachingMinTTL>300</ErrorCachingMinTTL>
|
|
1289
|
+
</CustomErrorResponse>
|
|
1290
|
+
</Items>
|
|
1291
|
+
</CustomErrorResponses>
|
|
1292
|
+
</DistributionConfig>`
|
|
1293
|
+
|
|
1294
|
+
const result = await this.client.request({
|
|
1295
|
+
service: 'cloudfront',
|
|
1296
|
+
region: 'us-east-1',
|
|
1297
|
+
method: 'POST',
|
|
1298
|
+
path: '/2020-05-31/distribution',
|
|
1299
|
+
body,
|
|
1300
|
+
headers: {
|
|
1301
|
+
'Content-Type': 'application/xml',
|
|
1302
|
+
},
|
|
1303
|
+
returnHeaders: true,
|
|
1304
|
+
})
|
|
1305
|
+
|
|
1306
|
+
const dist = result.body?.Distribution || result.Distribution || result.body || result
|
|
1307
|
+
|
|
1308
|
+
return {
|
|
1309
|
+
Id: dist.Id,
|
|
1310
|
+
ARN: dist.ARN,
|
|
1311
|
+
DomainName: dist.DomainName,
|
|
1312
|
+
Status: dist.Status,
|
|
1313
|
+
ETag: result.headers?.etag || result.ETag || '',
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Get S3 bucket policy for CloudFront OAC access
|
|
1319
|
+
*/
|
|
1320
|
+
static getS3BucketPolicyForCloudFront(bucketName: string, distributionArn: string): object {
|
|
1321
|
+
return {
|
|
1322
|
+
Version: '2012-10-17',
|
|
1323
|
+
Statement: [
|
|
1324
|
+
{
|
|
1325
|
+
Sid: 'AllowCloudFrontServicePrincipal',
|
|
1326
|
+
Effect: 'Allow',
|
|
1327
|
+
Principal: {
|
|
1328
|
+
Service: 'cloudfront.amazonaws.com',
|
|
1329
|
+
},
|
|
1330
|
+
Action: 's3:GetObject',
|
|
1331
|
+
Resource: `arn:aws:s3:::${bucketName}/*`,
|
|
1332
|
+
Condition: {
|
|
1333
|
+
StringEquals: {
|
|
1334
|
+
'AWS:SourceArn': distributionArn,
|
|
1335
|
+
},
|
|
1336
|
+
},
|
|
1337
|
+
},
|
|
1338
|
+
],
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Wait for distribution to be deployed
|
|
1344
|
+
*/
|
|
1345
|
+
async waitForDistributionDeployed(distributionId: string, maxAttempts = 60): Promise<boolean> {
|
|
1346
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
1347
|
+
const dist = await this.getDistribution(distributionId)
|
|
1348
|
+
|
|
1349
|
+
if (dist.Status === 'Deployed') {
|
|
1350
|
+
return true
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Wait 30 seconds between checks
|
|
1354
|
+
await new Promise(resolve => setTimeout(resolve, 30000))
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
return false
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Disable a CloudFront distribution
|
|
1362
|
+
* Must be disabled before it can be deleted
|
|
1363
|
+
*/
|
|
1364
|
+
async disableDistribution(distributionId: string): Promise<{ ETag: string }> {
|
|
1365
|
+
// Get current config with ETag
|
|
1366
|
+
const getResult = await this.client.request({
|
|
1367
|
+
service: 'cloudfront',
|
|
1368
|
+
region: 'us-east-1',
|
|
1369
|
+
method: 'GET',
|
|
1370
|
+
path: `/2020-05-31/distribution/${distributionId}/config`,
|
|
1371
|
+
returnHeaders: true,
|
|
1372
|
+
})
|
|
1373
|
+
|
|
1374
|
+
const etag = getResult.headers?.etag || getResult.headers?.ETag || ''
|
|
1375
|
+
const currentConfig = getResult.body?.DistributionConfig || getResult.DistributionConfig
|
|
1376
|
+
|
|
1377
|
+
if (!currentConfig) {
|
|
1378
|
+
throw new Error('Failed to get current distribution config')
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Set enabled to false
|
|
1382
|
+
currentConfig.Enabled = false
|
|
1383
|
+
|
|
1384
|
+
// Build the XML for the update request
|
|
1385
|
+
const configXml = this.buildDistributionConfigXml(currentConfig)
|
|
1386
|
+
|
|
1387
|
+
// Update the distribution
|
|
1388
|
+
const result = await this.client.request({
|
|
1389
|
+
service: 'cloudfront',
|
|
1390
|
+
region: 'us-east-1',
|
|
1391
|
+
method: 'PUT',
|
|
1392
|
+
path: `/2020-05-31/distribution/${distributionId}/config`,
|
|
1393
|
+
body: configXml,
|
|
1394
|
+
headers: {
|
|
1395
|
+
'Content-Type': 'application/xml',
|
|
1396
|
+
'If-Match': etag,
|
|
1397
|
+
},
|
|
1398
|
+
returnHeaders: true,
|
|
1399
|
+
})
|
|
1400
|
+
|
|
1401
|
+
return { ETag: result.headers?.etag || result.headers?.ETag || result.ETag || '' }
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* Delete a CloudFront distribution
|
|
1406
|
+
* Distribution must be disabled first
|
|
1407
|
+
*/
|
|
1408
|
+
async deleteDistribution(distributionId: string, etag?: string): Promise<void> {
|
|
1409
|
+
// If no ETag provided, get it first
|
|
1410
|
+
let etagToUse = etag || ''
|
|
1411
|
+
if (!etagToUse) {
|
|
1412
|
+
const getResult = await this.client.request({
|
|
1413
|
+
service: 'cloudfront',
|
|
1414
|
+
region: 'us-east-1',
|
|
1415
|
+
method: 'GET',
|
|
1416
|
+
path: `/2020-05-31/distribution/${distributionId}`,
|
|
1417
|
+
returnHeaders: true,
|
|
1418
|
+
})
|
|
1419
|
+
etagToUse = getResult.headers?.etag || getResult.headers?.ETag || ''
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
await this.client.request({
|
|
1423
|
+
service: 'cloudfront',
|
|
1424
|
+
region: 'us-east-1',
|
|
1425
|
+
method: 'DELETE',
|
|
1426
|
+
path: `/2020-05-31/distribution/${distributionId}`,
|
|
1427
|
+
headers: {
|
|
1428
|
+
'If-Match': etagToUse,
|
|
1429
|
+
},
|
|
1430
|
+
})
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* Wait for distribution to be disabled (ready for deletion)
|
|
1435
|
+
*/
|
|
1436
|
+
async waitForDistributionDisabled(distributionId: string, maxAttempts = 60): Promise<boolean> {
|
|
1437
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
1438
|
+
const dist = await this.getDistribution(distributionId)
|
|
1439
|
+
|
|
1440
|
+
if (dist.Status === 'Deployed' && !dist.Enabled) {
|
|
1441
|
+
return true
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// Wait 30 seconds between checks
|
|
1445
|
+
await new Promise(resolve => setTimeout(resolve, 30000))
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
return false
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* Remove a specific alias (CNAME) from a CloudFront distribution
|
|
1453
|
+
* This allows the alias to be used by another distribution
|
|
1454
|
+
*/
|
|
1455
|
+
async removeAlias(distributionId: string, alias: string): Promise<{ ETag: string }> {
|
|
1456
|
+
// Get current config with ETag
|
|
1457
|
+
const getResult = await this.client.request({
|
|
1458
|
+
service: 'cloudfront',
|
|
1459
|
+
region: 'us-east-1',
|
|
1460
|
+
method: 'GET',
|
|
1461
|
+
path: `/2020-05-31/distribution/${distributionId}/config`,
|
|
1462
|
+
returnHeaders: true,
|
|
1463
|
+
})
|
|
1464
|
+
|
|
1465
|
+
const etag = getResult.headers?.etag || getResult.headers?.ETag || ''
|
|
1466
|
+
const currentConfig = getResult.body?.DistributionConfig || getResult.DistributionConfig
|
|
1467
|
+
|
|
1468
|
+
if (!currentConfig) {
|
|
1469
|
+
throw new Error('Failed to get current distribution config')
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// Remove the alias from the Aliases list
|
|
1473
|
+
// Handle various structures: Items can be an array, or Items.CNAME can be a string or array
|
|
1474
|
+
let items: string[] = []
|
|
1475
|
+
if (currentConfig.Aliases?.Items) {
|
|
1476
|
+
if (Array.isArray(currentConfig.Aliases.Items)) {
|
|
1477
|
+
items = currentConfig.Aliases.Items
|
|
1478
|
+
}
|
|
1479
|
+
else if (typeof currentConfig.Aliases.Items === 'object') {
|
|
1480
|
+
const cname = currentConfig.Aliases.Items.CNAME
|
|
1481
|
+
if (typeof cname === 'string') {
|
|
1482
|
+
items = [cname]
|
|
1483
|
+
}
|
|
1484
|
+
else if (Array.isArray(cname)) {
|
|
1485
|
+
items = cname
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (items.length === 0) {
|
|
1491
|
+
throw new Error(`Distribution has no aliases to remove`)
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
const newItems = items.filter((a: string) => a !== alias)
|
|
1495
|
+
|
|
1496
|
+
if (newItems.length === items.length) {
|
|
1497
|
+
throw new Error(`Alias ${alias} not found in distribution`)
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
currentConfig.Aliases.Quantity = newItems.length
|
|
1501
|
+
// CloudFront expects Items to be an array, not Items.CNAME
|
|
1502
|
+
currentConfig.Aliases.Items = newItems.length > 0 ? newItems : undefined
|
|
1503
|
+
|
|
1504
|
+
// If removing the last alias, we need to also remove the ViewerCertificate ACM config
|
|
1505
|
+
if (newItems.length === 0) {
|
|
1506
|
+
currentConfig.ViewerCertificate = {
|
|
1507
|
+
CloudFrontDefaultCertificate: true,
|
|
1508
|
+
MinimumProtocolVersion: 'TLSv1.2_2021',
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// Build the XML for the update request
|
|
1513
|
+
const configXml = this.buildDistributionConfigXml(currentConfig)
|
|
1514
|
+
|
|
1515
|
+
// Update the distribution
|
|
1516
|
+
const result = await this.client.request({
|
|
1517
|
+
service: 'cloudfront',
|
|
1518
|
+
region: 'us-east-1',
|
|
1519
|
+
method: 'PUT',
|
|
1520
|
+
path: `/2020-05-31/distribution/${distributionId}/config`,
|
|
1521
|
+
body: configXml,
|
|
1522
|
+
headers: {
|
|
1523
|
+
'Content-Type': 'application/xml',
|
|
1524
|
+
'If-Match': etag,
|
|
1525
|
+
},
|
|
1526
|
+
returnHeaders: true,
|
|
1527
|
+
})
|
|
1528
|
+
|
|
1529
|
+
return { ETag: result.headers?.etag || result.headers?.ETag || result.ETag || '' }
|
|
1530
|
+
}
|
|
1531
|
+
}
|