@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,1046 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route53 Client - DNS management without AWS SDK
|
|
3
|
+
* Uses direct AWS API calls with Signature V4
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AWSClient } from './client'
|
|
7
|
+
|
|
8
|
+
export interface HostedZone {
|
|
9
|
+
Id: string
|
|
10
|
+
Name: string
|
|
11
|
+
CallerReference?: string
|
|
12
|
+
Config?: {
|
|
13
|
+
Comment?: string
|
|
14
|
+
PrivateZone?: boolean
|
|
15
|
+
}
|
|
16
|
+
ResourceRecordSetCount?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ResourceRecordSet {
|
|
20
|
+
Name: string
|
|
21
|
+
Type: string
|
|
22
|
+
TTL?: number
|
|
23
|
+
ResourceRecords?: { Value: string }[]
|
|
24
|
+
AliasTarget?: {
|
|
25
|
+
HostedZoneId: string
|
|
26
|
+
DNSName: string
|
|
27
|
+
EvaluateTargetHealth: boolean
|
|
28
|
+
}
|
|
29
|
+
SetIdentifier?: string
|
|
30
|
+
Weight?: number
|
|
31
|
+
Region?: string
|
|
32
|
+
GeoLocation?: {
|
|
33
|
+
ContinentCode?: string
|
|
34
|
+
CountryCode?: string
|
|
35
|
+
SubdivisionCode?: string
|
|
36
|
+
}
|
|
37
|
+
Failover?: 'PRIMARY' | 'SECONDARY'
|
|
38
|
+
HealthCheckId?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DelegationSet {
|
|
42
|
+
Id?: string
|
|
43
|
+
CallerReference?: string
|
|
44
|
+
NameServers: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CreateHostedZoneResult {
|
|
48
|
+
HostedZone: HostedZone
|
|
49
|
+
ChangeInfo: {
|
|
50
|
+
Id: string
|
|
51
|
+
Status: string
|
|
52
|
+
SubmittedAt: string
|
|
53
|
+
}
|
|
54
|
+
DelegationSet: DelegationSet
|
|
55
|
+
Location: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ListHostedZonesResult {
|
|
59
|
+
HostedZones: HostedZone[]
|
|
60
|
+
IsTruncated: boolean
|
|
61
|
+
MaxItems: string
|
|
62
|
+
Marker?: string
|
|
63
|
+
NextMarker?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface GetHostedZoneResult {
|
|
67
|
+
HostedZone: HostedZone
|
|
68
|
+
DelegationSet: DelegationSet
|
|
69
|
+
VPCs?: { VPCId: string, VPCRegion: string }[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ListResourceRecordSetsResult {
|
|
73
|
+
ResourceRecordSets: ResourceRecordSet[]
|
|
74
|
+
IsTruncated: boolean
|
|
75
|
+
MaxItems: string
|
|
76
|
+
NextRecordName?: string
|
|
77
|
+
NextRecordType?: string
|
|
78
|
+
NextRecordIdentifier?: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ChangeResourceRecordSetsResult {
|
|
82
|
+
ChangeInfo: {
|
|
83
|
+
Id: string
|
|
84
|
+
Status: string
|
|
85
|
+
SubmittedAt: string
|
|
86
|
+
Comment?: string
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface Change {
|
|
91
|
+
Action: 'CREATE' | 'DELETE' | 'UPSERT'
|
|
92
|
+
ResourceRecordSet: ResourceRecordSet
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ChangeBatch {
|
|
96
|
+
Comment?: string
|
|
97
|
+
Changes: Change[]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Route53 Client for DNS management
|
|
102
|
+
*/
|
|
103
|
+
export class Route53Client {
|
|
104
|
+
private client: AWSClient
|
|
105
|
+
private region: string
|
|
106
|
+
|
|
107
|
+
constructor(region: string = 'us-east-1') {
|
|
108
|
+
this.region = region
|
|
109
|
+
this.client = new AWSClient()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a new hosted zone
|
|
114
|
+
*/
|
|
115
|
+
async createHostedZone(params: {
|
|
116
|
+
Name: string
|
|
117
|
+
CallerReference?: string
|
|
118
|
+
HostedZoneConfig?: {
|
|
119
|
+
Comment?: string
|
|
120
|
+
PrivateZone?: boolean
|
|
121
|
+
}
|
|
122
|
+
VPC?: {
|
|
123
|
+
VPCRegion: string
|
|
124
|
+
VPCId: string
|
|
125
|
+
}
|
|
126
|
+
DelegationSetId?: string
|
|
127
|
+
}): Promise<CreateHostedZoneResult> {
|
|
128
|
+
const callerReference = params.CallerReference || `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
129
|
+
|
|
130
|
+
// Build XML request body
|
|
131
|
+
let xmlBody = `<?xml version="1.0" encoding="UTF-8"?>
|
|
132
|
+
<CreateHostedZoneRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
133
|
+
<Name>${params.Name}</Name>
|
|
134
|
+
<CallerReference>${callerReference}</CallerReference>`
|
|
135
|
+
|
|
136
|
+
if (params.HostedZoneConfig) {
|
|
137
|
+
xmlBody += `
|
|
138
|
+
<HostedZoneConfig>`
|
|
139
|
+
if (params.HostedZoneConfig.Comment) {
|
|
140
|
+
xmlBody += `
|
|
141
|
+
<Comment>${params.HostedZoneConfig.Comment}</Comment>`
|
|
142
|
+
}
|
|
143
|
+
if (params.HostedZoneConfig.PrivateZone !== undefined) {
|
|
144
|
+
xmlBody += `
|
|
145
|
+
<PrivateZone>${params.HostedZoneConfig.PrivateZone}</PrivateZone>`
|
|
146
|
+
}
|
|
147
|
+
xmlBody += `
|
|
148
|
+
</HostedZoneConfig>`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (params.VPC) {
|
|
152
|
+
xmlBody += `
|
|
153
|
+
<VPC>
|
|
154
|
+
<VPCRegion>${params.VPC.VPCRegion}</VPCRegion>
|
|
155
|
+
<VPCId>${params.VPC.VPCId}</VPCId>
|
|
156
|
+
</VPC>`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (params.DelegationSetId) {
|
|
160
|
+
xmlBody += `
|
|
161
|
+
<DelegationSetId>${params.DelegationSetId}</DelegationSetId>`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
xmlBody += `
|
|
165
|
+
</CreateHostedZoneRequest>`
|
|
166
|
+
|
|
167
|
+
const result = await this.client.request({
|
|
168
|
+
service: 'route53',
|
|
169
|
+
region: this.region,
|
|
170
|
+
method: 'POST',
|
|
171
|
+
path: '/2013-04-01/hostedzone',
|
|
172
|
+
headers: {
|
|
173
|
+
'content-type': 'application/xml',
|
|
174
|
+
},
|
|
175
|
+
body: xmlBody,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
return this.parseCreateHostedZoneResponse(result)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* List hosted zones
|
|
183
|
+
*/
|
|
184
|
+
async listHostedZones(params?: {
|
|
185
|
+
Marker?: string
|
|
186
|
+
MaxItems?: string
|
|
187
|
+
}): Promise<ListHostedZonesResult> {
|
|
188
|
+
const queryParams: Record<string, string> = {}
|
|
189
|
+
|
|
190
|
+
if (params?.Marker) queryParams.marker = params.Marker
|
|
191
|
+
if (params?.MaxItems) queryParams.maxitems = params.MaxItems
|
|
192
|
+
|
|
193
|
+
const result = await this.client.request({
|
|
194
|
+
service: 'route53',
|
|
195
|
+
region: this.region,
|
|
196
|
+
method: 'GET',
|
|
197
|
+
path: '/2013-04-01/hostedzone',
|
|
198
|
+
queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
return this.parseListHostedZonesResponse(result)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* List hosted zones by name (more efficient for finding specific zone)
|
|
206
|
+
*/
|
|
207
|
+
async listHostedZonesByName(params?: {
|
|
208
|
+
DNSName?: string
|
|
209
|
+
HostedZoneId?: string
|
|
210
|
+
MaxItems?: string
|
|
211
|
+
}): Promise<ListHostedZonesResult> {
|
|
212
|
+
const queryParams: Record<string, string> = {}
|
|
213
|
+
|
|
214
|
+
if (params?.DNSName) queryParams.dnsname = params.DNSName
|
|
215
|
+
if (params?.HostedZoneId) queryParams.hostedzoneid = params.HostedZoneId
|
|
216
|
+
if (params?.MaxItems) queryParams.maxitems = params.MaxItems
|
|
217
|
+
|
|
218
|
+
const result = await this.client.request({
|
|
219
|
+
service: 'route53',
|
|
220
|
+
region: this.region,
|
|
221
|
+
method: 'GET',
|
|
222
|
+
path: '/2013-04-01/hostedzonesbyname',
|
|
223
|
+
queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
return this.parseListHostedZonesResponse(result)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get a hosted zone
|
|
231
|
+
*/
|
|
232
|
+
async getHostedZone(params: {
|
|
233
|
+
Id: string
|
|
234
|
+
}): Promise<GetHostedZoneResult> {
|
|
235
|
+
// Strip /hostedzone/ prefix if present
|
|
236
|
+
const hostedZoneId = params.Id.replace('/hostedzone/', '')
|
|
237
|
+
|
|
238
|
+
const result = await this.client.request({
|
|
239
|
+
service: 'route53',
|
|
240
|
+
region: this.region,
|
|
241
|
+
method: 'GET',
|
|
242
|
+
path: `/2013-04-01/hostedzone/${hostedZoneId}`,
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
return this.parseGetHostedZoneResponse(result)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Delete a hosted zone
|
|
250
|
+
*/
|
|
251
|
+
async deleteHostedZone(params: {
|
|
252
|
+
Id: string
|
|
253
|
+
}): Promise<void> {
|
|
254
|
+
// Strip /hostedzone/ prefix if present
|
|
255
|
+
const hostedZoneId = params.Id.replace('/hostedzone/', '')
|
|
256
|
+
|
|
257
|
+
await this.client.request({
|
|
258
|
+
service: 'route53',
|
|
259
|
+
region: this.region,
|
|
260
|
+
method: 'DELETE',
|
|
261
|
+
path: `/2013-04-01/hostedzone/${hostedZoneId}`,
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* List resource record sets in a hosted zone
|
|
267
|
+
*/
|
|
268
|
+
async listResourceRecordSets(params: {
|
|
269
|
+
HostedZoneId: string
|
|
270
|
+
StartRecordName?: string
|
|
271
|
+
StartRecordType?: string
|
|
272
|
+
StartRecordIdentifier?: string
|
|
273
|
+
MaxItems?: string
|
|
274
|
+
}): Promise<ListResourceRecordSetsResult> {
|
|
275
|
+
// Strip /hostedzone/ prefix if present
|
|
276
|
+
const hostedZoneId = params.HostedZoneId.replace('/hostedzone/', '')
|
|
277
|
+
|
|
278
|
+
const queryParams: Record<string, string> = {}
|
|
279
|
+
|
|
280
|
+
if (params.StartRecordName) queryParams.name = params.StartRecordName
|
|
281
|
+
if (params.StartRecordType) queryParams.type = params.StartRecordType
|
|
282
|
+
if (params.StartRecordIdentifier) queryParams.identifier = params.StartRecordIdentifier
|
|
283
|
+
if (params.MaxItems) queryParams.maxitems = params.MaxItems
|
|
284
|
+
|
|
285
|
+
const result = await this.client.request({
|
|
286
|
+
service: 'route53',
|
|
287
|
+
region: this.region,
|
|
288
|
+
method: 'GET',
|
|
289
|
+
path: `/2013-04-01/hostedzone/${hostedZoneId}/rrset`,
|
|
290
|
+
queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
return this.parseListResourceRecordSetsResponse(result)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Change resource record sets (create, delete, or upsert)
|
|
298
|
+
*/
|
|
299
|
+
async changeResourceRecordSets(params: {
|
|
300
|
+
HostedZoneId: string
|
|
301
|
+
ChangeBatch: ChangeBatch
|
|
302
|
+
}): Promise<ChangeResourceRecordSetsResult> {
|
|
303
|
+
// Strip /hostedzone/ prefix if present
|
|
304
|
+
const hostedZoneId = params.HostedZoneId.replace('/hostedzone/', '')
|
|
305
|
+
|
|
306
|
+
// Build XML request body
|
|
307
|
+
let xmlBody = `<?xml version="1.0" encoding="UTF-8"?>
|
|
308
|
+
<ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
309
|
+
<ChangeBatch>`
|
|
310
|
+
|
|
311
|
+
if (params.ChangeBatch.Comment) {
|
|
312
|
+
xmlBody += `
|
|
313
|
+
<Comment>${this.escapeXml(params.ChangeBatch.Comment)}</Comment>`
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
xmlBody += `
|
|
317
|
+
<Changes>`
|
|
318
|
+
|
|
319
|
+
for (const change of params.ChangeBatch.Changes) {
|
|
320
|
+
xmlBody += `
|
|
321
|
+
<Change>
|
|
322
|
+
<Action>${change.Action}</Action>
|
|
323
|
+
<ResourceRecordSet>
|
|
324
|
+
<Name>${change.ResourceRecordSet.Name}</Name>
|
|
325
|
+
<Type>${change.ResourceRecordSet.Type}</Type>`
|
|
326
|
+
|
|
327
|
+
if (change.ResourceRecordSet.TTL !== undefined) {
|
|
328
|
+
xmlBody += `
|
|
329
|
+
<TTL>${change.ResourceRecordSet.TTL}</TTL>`
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (change.ResourceRecordSet.SetIdentifier) {
|
|
333
|
+
xmlBody += `
|
|
334
|
+
<SetIdentifier>${change.ResourceRecordSet.SetIdentifier}</SetIdentifier>`
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (change.ResourceRecordSet.Weight !== undefined) {
|
|
338
|
+
xmlBody += `
|
|
339
|
+
<Weight>${change.ResourceRecordSet.Weight}</Weight>`
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (change.ResourceRecordSet.Region) {
|
|
343
|
+
xmlBody += `
|
|
344
|
+
<Region>${change.ResourceRecordSet.Region}</Region>`
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (change.ResourceRecordSet.Failover) {
|
|
348
|
+
xmlBody += `
|
|
349
|
+
<Failover>${change.ResourceRecordSet.Failover}</Failover>`
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (change.ResourceRecordSet.HealthCheckId) {
|
|
353
|
+
xmlBody += `
|
|
354
|
+
<HealthCheckId>${change.ResourceRecordSet.HealthCheckId}</HealthCheckId>`
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (change.ResourceRecordSet.ResourceRecords && change.ResourceRecordSet.ResourceRecords.length > 0) {
|
|
358
|
+
xmlBody += `
|
|
359
|
+
<ResourceRecords>`
|
|
360
|
+
for (const record of change.ResourceRecordSet.ResourceRecords) {
|
|
361
|
+
xmlBody += `
|
|
362
|
+
<ResourceRecord>
|
|
363
|
+
<Value>${this.escapeXml(record.Value)}</Value>
|
|
364
|
+
</ResourceRecord>`
|
|
365
|
+
}
|
|
366
|
+
xmlBody += `
|
|
367
|
+
</ResourceRecords>`
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (change.ResourceRecordSet.AliasTarget) {
|
|
371
|
+
xmlBody += `
|
|
372
|
+
<AliasTarget>
|
|
373
|
+
<HostedZoneId>${change.ResourceRecordSet.AliasTarget.HostedZoneId}</HostedZoneId>
|
|
374
|
+
<DNSName>${change.ResourceRecordSet.AliasTarget.DNSName}</DNSName>
|
|
375
|
+
<EvaluateTargetHealth>${change.ResourceRecordSet.AliasTarget.EvaluateTargetHealth}</EvaluateTargetHealth>
|
|
376
|
+
</AliasTarget>`
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (change.ResourceRecordSet.GeoLocation) {
|
|
380
|
+
xmlBody += `
|
|
381
|
+
<GeoLocation>`
|
|
382
|
+
if (change.ResourceRecordSet.GeoLocation.ContinentCode) {
|
|
383
|
+
xmlBody += `
|
|
384
|
+
<ContinentCode>${change.ResourceRecordSet.GeoLocation.ContinentCode}</ContinentCode>`
|
|
385
|
+
}
|
|
386
|
+
if (change.ResourceRecordSet.GeoLocation.CountryCode) {
|
|
387
|
+
xmlBody += `
|
|
388
|
+
<CountryCode>${change.ResourceRecordSet.GeoLocation.CountryCode}</CountryCode>`
|
|
389
|
+
}
|
|
390
|
+
if (change.ResourceRecordSet.GeoLocation.SubdivisionCode) {
|
|
391
|
+
xmlBody += `
|
|
392
|
+
<SubdivisionCode>${change.ResourceRecordSet.GeoLocation.SubdivisionCode}</SubdivisionCode>`
|
|
393
|
+
}
|
|
394
|
+
xmlBody += `
|
|
395
|
+
</GeoLocation>`
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
xmlBody += `
|
|
399
|
+
</ResourceRecordSet>
|
|
400
|
+
</Change>`
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
xmlBody += `
|
|
404
|
+
</Changes>
|
|
405
|
+
</ChangeBatch>
|
|
406
|
+
</ChangeResourceRecordSetsRequest>`
|
|
407
|
+
|
|
408
|
+
const result = await this.client.request({
|
|
409
|
+
service: 'route53',
|
|
410
|
+
region: this.region,
|
|
411
|
+
method: 'POST',
|
|
412
|
+
path: `/2013-04-01/hostedzone/${hostedZoneId}/rrset`,
|
|
413
|
+
headers: {
|
|
414
|
+
'content-type': 'application/xml',
|
|
415
|
+
},
|
|
416
|
+
body: xmlBody,
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
return this.parseChangeResourceRecordSetsResponse(result)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Escape XML special characters
|
|
424
|
+
*/
|
|
425
|
+
private escapeXml(str: string): string {
|
|
426
|
+
return str
|
|
427
|
+
.replace(/&/g, '&')
|
|
428
|
+
.replace(/</g, '<')
|
|
429
|
+
.replace(/>/g, '>')
|
|
430
|
+
.replace(/"/g, '"')
|
|
431
|
+
.replace(/'/g, ''')
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Parse CreateHostedZone response
|
|
436
|
+
*/
|
|
437
|
+
private parseCreateHostedZoneResponse(result: any): CreateHostedZoneResult {
|
|
438
|
+
const response = result.CreateHostedZoneResponse || result
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
HostedZone: this.parseHostedZone(response.HostedZone),
|
|
442
|
+
ChangeInfo: {
|
|
443
|
+
Id: response.ChangeInfo?.Id || '',
|
|
444
|
+
Status: response.ChangeInfo?.Status || '',
|
|
445
|
+
SubmittedAt: response.ChangeInfo?.SubmittedAt || '',
|
|
446
|
+
},
|
|
447
|
+
DelegationSet: this.parseDelegationSet(response.DelegationSet),
|
|
448
|
+
Location: response.Location || '',
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Parse ListHostedZones response
|
|
454
|
+
*/
|
|
455
|
+
private parseListHostedZonesResponse(result: any): ListHostedZonesResult {
|
|
456
|
+
const response = result.ListHostedZonesResponse || result.ListHostedZonesByNameResponse || result
|
|
457
|
+
|
|
458
|
+
let hostedZones = response.HostedZones?.HostedZone || response.HostedZones || []
|
|
459
|
+
|
|
460
|
+
// Ensure it's an array
|
|
461
|
+
if (!Array.isArray(hostedZones)) {
|
|
462
|
+
hostedZones = hostedZones ? [hostedZones] : []
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
HostedZones: hostedZones.map((hz: any) => this.parseHostedZone(hz)),
|
|
467
|
+
IsTruncated: response.IsTruncated === 'true' || response.IsTruncated === true,
|
|
468
|
+
MaxItems: response.MaxItems || '100',
|
|
469
|
+
Marker: response.Marker,
|
|
470
|
+
NextMarker: response.NextMarker,
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Parse GetHostedZone response
|
|
476
|
+
*/
|
|
477
|
+
private parseGetHostedZoneResponse(result: any): GetHostedZoneResult {
|
|
478
|
+
const response = result.GetHostedZoneResponse || result
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
HostedZone: this.parseHostedZone(response.HostedZone),
|
|
482
|
+
DelegationSet: this.parseDelegationSet(response.DelegationSet),
|
|
483
|
+
VPCs: response.VPCs?.VPC ? (Array.isArray(response.VPCs.VPC) ? response.VPCs.VPC : [response.VPCs.VPC]) : undefined,
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Parse ListResourceRecordSets response
|
|
489
|
+
*/
|
|
490
|
+
private parseListResourceRecordSetsResponse(result: any): ListResourceRecordSetsResult {
|
|
491
|
+
const response = result.ListResourceRecordSetsResponse || result
|
|
492
|
+
|
|
493
|
+
let recordSets = response.ResourceRecordSets?.ResourceRecordSet || response.ResourceRecordSets || []
|
|
494
|
+
|
|
495
|
+
// Ensure it's an array
|
|
496
|
+
if (!Array.isArray(recordSets)) {
|
|
497
|
+
recordSets = recordSets ? [recordSets] : []
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
ResourceRecordSets: recordSets.map((rs: any) => this.parseResourceRecordSet(rs)),
|
|
502
|
+
IsTruncated: response.IsTruncated === 'true' || response.IsTruncated === true,
|
|
503
|
+
MaxItems: response.MaxItems || '100',
|
|
504
|
+
NextRecordName: response.NextRecordName,
|
|
505
|
+
NextRecordType: response.NextRecordType,
|
|
506
|
+
NextRecordIdentifier: response.NextRecordIdentifier,
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Parse ChangeResourceRecordSets response
|
|
512
|
+
*/
|
|
513
|
+
private parseChangeResourceRecordSetsResponse(result: any): ChangeResourceRecordSetsResult {
|
|
514
|
+
const response = result.ChangeResourceRecordSetsResponse || result
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
ChangeInfo: {
|
|
518
|
+
Id: response.ChangeInfo?.Id || '',
|
|
519
|
+
Status: response.ChangeInfo?.Status || '',
|
|
520
|
+
SubmittedAt: response.ChangeInfo?.SubmittedAt || '',
|
|
521
|
+
Comment: response.ChangeInfo?.Comment,
|
|
522
|
+
},
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Parse a hosted zone object
|
|
528
|
+
*/
|
|
529
|
+
private parseHostedZone(hz: any): HostedZone {
|
|
530
|
+
if (!hz) return { Id: '', Name: '' }
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
Id: hz.Id || '',
|
|
534
|
+
Name: hz.Name || '',
|
|
535
|
+
CallerReference: hz.CallerReference,
|
|
536
|
+
Config: hz.Config ? {
|
|
537
|
+
Comment: hz.Config.Comment,
|
|
538
|
+
PrivateZone: hz.Config.PrivateZone === 'true' || hz.Config.PrivateZone === true,
|
|
539
|
+
} : undefined,
|
|
540
|
+
ResourceRecordSetCount: hz.ResourceRecordSetCount ? Number(hz.ResourceRecordSetCount) : undefined,
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Parse a delegation set object
|
|
546
|
+
*/
|
|
547
|
+
private parseDelegationSet(ds: any): DelegationSet {
|
|
548
|
+
if (!ds) return { NameServers: [] }
|
|
549
|
+
|
|
550
|
+
let nameServers = ds.NameServers?.NameServer || ds.NameServers || []
|
|
551
|
+
|
|
552
|
+
// Ensure it's an array
|
|
553
|
+
if (!Array.isArray(nameServers)) {
|
|
554
|
+
nameServers = nameServers ? [nameServers] : []
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
Id: ds.Id,
|
|
559
|
+
CallerReference: ds.CallerReference,
|
|
560
|
+
NameServers: nameServers,
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Parse a resource record set object
|
|
566
|
+
*/
|
|
567
|
+
private parseResourceRecordSet(rs: any): ResourceRecordSet {
|
|
568
|
+
if (!rs) return { Name: '', Type: '' }
|
|
569
|
+
|
|
570
|
+
let resourceRecords = rs.ResourceRecords?.ResourceRecord || rs.ResourceRecords || []
|
|
571
|
+
|
|
572
|
+
// Ensure it's an array
|
|
573
|
+
if (!Array.isArray(resourceRecords)) {
|
|
574
|
+
resourceRecords = resourceRecords ? [resourceRecords] : []
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
Name: rs.Name || '',
|
|
579
|
+
Type: rs.Type || '',
|
|
580
|
+
TTL: rs.TTL ? Number(rs.TTL) : undefined,
|
|
581
|
+
ResourceRecords: resourceRecords.map((rr: any) => ({
|
|
582
|
+
Value: rr.Value || rr,
|
|
583
|
+
})),
|
|
584
|
+
AliasTarget: rs.AliasTarget ? {
|
|
585
|
+
HostedZoneId: rs.AliasTarget.HostedZoneId,
|
|
586
|
+
DNSName: rs.AliasTarget.DNSName,
|
|
587
|
+
EvaluateTargetHealth: rs.AliasTarget.EvaluateTargetHealth === 'true' || rs.AliasTarget.EvaluateTargetHealth === true,
|
|
588
|
+
} : undefined,
|
|
589
|
+
SetIdentifier: rs.SetIdentifier,
|
|
590
|
+
Weight: rs.Weight ? Number(rs.Weight) : undefined,
|
|
591
|
+
Region: rs.Region,
|
|
592
|
+
GeoLocation: rs.GeoLocation,
|
|
593
|
+
Failover: rs.Failover,
|
|
594
|
+
HealthCheckId: rs.HealthCheckId,
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Helper methods for common operations
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Find hosted zone by domain name
|
|
602
|
+
*/
|
|
603
|
+
async findHostedZoneByName(domainName: string): Promise<HostedZone | null> {
|
|
604
|
+
// Ensure domain ends with a dot
|
|
605
|
+
const normalizedDomain = domainName.endsWith('.') ? domainName : `${domainName}.`
|
|
606
|
+
|
|
607
|
+
const result = await this.listHostedZonesByName({ DNSName: normalizedDomain })
|
|
608
|
+
const zone = result.HostedZones.find(z => z.Name === normalizedDomain)
|
|
609
|
+
|
|
610
|
+
return zone || null
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Create an A record
|
|
615
|
+
*/
|
|
616
|
+
async createARecord(params: {
|
|
617
|
+
HostedZoneId: string
|
|
618
|
+
Name: string
|
|
619
|
+
Value: string | string[]
|
|
620
|
+
TTL?: number
|
|
621
|
+
}): Promise<ChangeResourceRecordSetsResult> {
|
|
622
|
+
const values = Array.isArray(params.Value) ? params.Value : [params.Value]
|
|
623
|
+
|
|
624
|
+
return this.changeResourceRecordSets({
|
|
625
|
+
HostedZoneId: params.HostedZoneId,
|
|
626
|
+
ChangeBatch: {
|
|
627
|
+
Changes: [{
|
|
628
|
+
Action: 'UPSERT',
|
|
629
|
+
ResourceRecordSet: {
|
|
630
|
+
Name: params.Name,
|
|
631
|
+
Type: 'A',
|
|
632
|
+
TTL: params.TTL || 300,
|
|
633
|
+
ResourceRecords: values.map(v => ({ Value: v })),
|
|
634
|
+
},
|
|
635
|
+
}],
|
|
636
|
+
},
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Create a CNAME record
|
|
642
|
+
*/
|
|
643
|
+
async createCnameRecord(params: {
|
|
644
|
+
HostedZoneId: string
|
|
645
|
+
Name: string
|
|
646
|
+
Value: string
|
|
647
|
+
TTL?: number
|
|
648
|
+
}): Promise<ChangeResourceRecordSetsResult> {
|
|
649
|
+
return this.changeResourceRecordSets({
|
|
650
|
+
HostedZoneId: params.HostedZoneId,
|
|
651
|
+
ChangeBatch: {
|
|
652
|
+
Changes: [{
|
|
653
|
+
Action: 'UPSERT',
|
|
654
|
+
ResourceRecordSet: {
|
|
655
|
+
Name: params.Name,
|
|
656
|
+
Type: 'CNAME',
|
|
657
|
+
TTL: params.TTL || 300,
|
|
658
|
+
ResourceRecords: [{ Value: params.Value }],
|
|
659
|
+
},
|
|
660
|
+
}],
|
|
661
|
+
},
|
|
662
|
+
})
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Create an alias record (for CloudFront, ALB, etc.)
|
|
667
|
+
*/
|
|
668
|
+
async createAliasRecord(params: {
|
|
669
|
+
HostedZoneId: string
|
|
670
|
+
Name: string
|
|
671
|
+
TargetHostedZoneId: string
|
|
672
|
+
TargetDNSName: string
|
|
673
|
+
EvaluateTargetHealth?: boolean
|
|
674
|
+
Type?: 'A' | 'AAAA'
|
|
675
|
+
}): Promise<ChangeResourceRecordSetsResult> {
|
|
676
|
+
return this.changeResourceRecordSets({
|
|
677
|
+
HostedZoneId: params.HostedZoneId,
|
|
678
|
+
ChangeBatch: {
|
|
679
|
+
Changes: [{
|
|
680
|
+
Action: 'UPSERT',
|
|
681
|
+
ResourceRecordSet: {
|
|
682
|
+
Name: params.Name,
|
|
683
|
+
Type: params.Type || 'A',
|
|
684
|
+
AliasTarget: {
|
|
685
|
+
HostedZoneId: params.TargetHostedZoneId,
|
|
686
|
+
DNSName: params.TargetDNSName,
|
|
687
|
+
EvaluateTargetHealth: params.EvaluateTargetHealth ?? false,
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
}],
|
|
691
|
+
},
|
|
692
|
+
})
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Create a TXT record
|
|
697
|
+
*/
|
|
698
|
+
async createTxtRecord(params: {
|
|
699
|
+
HostedZoneId: string
|
|
700
|
+
Name: string
|
|
701
|
+
Value: string | string[]
|
|
702
|
+
TTL?: number
|
|
703
|
+
}): Promise<ChangeResourceRecordSetsResult> {
|
|
704
|
+
const values = Array.isArray(params.Value) ? params.Value : [params.Value]
|
|
705
|
+
// TXT records need to be quoted
|
|
706
|
+
const quotedValues = values.map((v) => {
|
|
707
|
+
if (!v.startsWith('"')) {
|
|
708
|
+
return `"${v}"`
|
|
709
|
+
}
|
|
710
|
+
return v
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
return this.changeResourceRecordSets({
|
|
714
|
+
HostedZoneId: params.HostedZoneId,
|
|
715
|
+
ChangeBatch: {
|
|
716
|
+
Changes: [{
|
|
717
|
+
Action: 'UPSERT',
|
|
718
|
+
ResourceRecordSet: {
|
|
719
|
+
Name: params.Name,
|
|
720
|
+
Type: 'TXT',
|
|
721
|
+
TTL: params.TTL || 300,
|
|
722
|
+
ResourceRecords: quotedValues.map(v => ({ Value: v })),
|
|
723
|
+
},
|
|
724
|
+
}],
|
|
725
|
+
},
|
|
726
|
+
})
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Create an MX record
|
|
731
|
+
*/
|
|
732
|
+
async createMxRecord(params: {
|
|
733
|
+
HostedZoneId: string
|
|
734
|
+
Name: string
|
|
735
|
+
Values: Array<{ priority: number, mailServer: string }>
|
|
736
|
+
TTL?: number
|
|
737
|
+
}): Promise<ChangeResourceRecordSetsResult> {
|
|
738
|
+
return this.changeResourceRecordSets({
|
|
739
|
+
HostedZoneId: params.HostedZoneId,
|
|
740
|
+
ChangeBatch: {
|
|
741
|
+
Changes: [{
|
|
742
|
+
Action: 'UPSERT',
|
|
743
|
+
ResourceRecordSet: {
|
|
744
|
+
Name: params.Name,
|
|
745
|
+
Type: 'MX',
|
|
746
|
+
TTL: params.TTL || 300,
|
|
747
|
+
ResourceRecords: params.Values.map(v => ({
|
|
748
|
+
Value: `${v.priority} ${v.mailServer}`,
|
|
749
|
+
})),
|
|
750
|
+
},
|
|
751
|
+
}],
|
|
752
|
+
},
|
|
753
|
+
})
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Delete a record
|
|
758
|
+
*/
|
|
759
|
+
async deleteRecord(params: {
|
|
760
|
+
HostedZoneId: string
|
|
761
|
+
RecordSet: ResourceRecordSet
|
|
762
|
+
}): Promise<ChangeResourceRecordSetsResult> {
|
|
763
|
+
return this.changeResourceRecordSets({
|
|
764
|
+
HostedZoneId: params.HostedZoneId,
|
|
765
|
+
ChangeBatch: {
|
|
766
|
+
Changes: [{
|
|
767
|
+
Action: 'DELETE',
|
|
768
|
+
ResourceRecordSet: params.RecordSet,
|
|
769
|
+
}],
|
|
770
|
+
},
|
|
771
|
+
})
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Wait for a change to become INSYNC
|
|
776
|
+
*/
|
|
777
|
+
async waitForChange(changeId: string, maxAttempts = 60, delayMs = 5000): Promise<boolean> {
|
|
778
|
+
const id = changeId.replace('/change/', '')
|
|
779
|
+
|
|
780
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
781
|
+
const result = await this.client.request({
|
|
782
|
+
service: 'route53',
|
|
783
|
+
region: this.region,
|
|
784
|
+
method: 'GET',
|
|
785
|
+
path: `/2013-04-01/change/${id}`,
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
const status = result.GetChangeResponse?.ChangeInfo?.Status || result.ChangeInfo?.Status
|
|
789
|
+
if (status === 'INSYNC') {
|
|
790
|
+
return true
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
await new Promise(resolve => setTimeout(resolve, delayMs))
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return false
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Find or create a hosted zone for a domain
|
|
801
|
+
* Automatically creates the zone if it doesn't exist
|
|
802
|
+
*/
|
|
803
|
+
async findOrCreateHostedZone(params: {
|
|
804
|
+
domainName: string
|
|
805
|
+
comment?: string
|
|
806
|
+
privateZone?: boolean
|
|
807
|
+
vpc?: {
|
|
808
|
+
VPCRegion: string
|
|
809
|
+
VPCId: string
|
|
810
|
+
}
|
|
811
|
+
}): Promise<{
|
|
812
|
+
hostedZone: HostedZone
|
|
813
|
+
nameServers: string[]
|
|
814
|
+
isNew: boolean
|
|
815
|
+
}> {
|
|
816
|
+
// Normalize domain (ensure it ends with a dot)
|
|
817
|
+
const normalizedDomain = params.domainName.endsWith('.')
|
|
818
|
+
? params.domainName
|
|
819
|
+
: `${params.domainName}.`
|
|
820
|
+
|
|
821
|
+
// First, try to find existing hosted zone
|
|
822
|
+
const existing = await this.findHostedZoneByName(normalizedDomain)
|
|
823
|
+
|
|
824
|
+
if (existing) {
|
|
825
|
+
// Get the delegation set for name servers
|
|
826
|
+
const zoneDetails = await this.getHostedZone({ Id: existing.Id })
|
|
827
|
+
return {
|
|
828
|
+
hostedZone: existing,
|
|
829
|
+
nameServers: zoneDetails.DelegationSet.NameServers,
|
|
830
|
+
isNew: false,
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Create new hosted zone
|
|
835
|
+
const result = await this.createHostedZone({
|
|
836
|
+
Name: normalizedDomain,
|
|
837
|
+
HostedZoneConfig: {
|
|
838
|
+
Comment: params.comment || `Hosted zone for ${params.domainName}`,
|
|
839
|
+
PrivateZone: params.privateZone,
|
|
840
|
+
},
|
|
841
|
+
VPC: params.vpc,
|
|
842
|
+
})
|
|
843
|
+
|
|
844
|
+
return {
|
|
845
|
+
hostedZone: result.HostedZone,
|
|
846
|
+
nameServers: result.DelegationSet.NameServers,
|
|
847
|
+
isNew: true,
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Get the root domain from a subdomain
|
|
853
|
+
* e.g., "api.example.com" -> "example.com"
|
|
854
|
+
*/
|
|
855
|
+
static getRootDomain(domain: string): string {
|
|
856
|
+
const parts = domain.replace(/\.$/, '').split('.')
|
|
857
|
+
if (parts.length <= 2) {
|
|
858
|
+
return domain
|
|
859
|
+
}
|
|
860
|
+
// Return last two parts (handles most TLDs)
|
|
861
|
+
return parts.slice(-2).join('.')
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Find the hosted zone for a domain or its parent domain
|
|
866
|
+
* Useful when you have a subdomain and need to find the zone
|
|
867
|
+
*/
|
|
868
|
+
async findHostedZoneForDomain(domain: string): Promise<HostedZone | null> {
|
|
869
|
+
const normalizedDomain = domain.replace(/\.$/, '')
|
|
870
|
+
const parts = normalizedDomain.split('.')
|
|
871
|
+
|
|
872
|
+
// Try from most specific to least specific
|
|
873
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
874
|
+
const testDomain = parts.slice(i).join('.')
|
|
875
|
+
const zone = await this.findHostedZoneByName(testDomain)
|
|
876
|
+
if (zone) {
|
|
877
|
+
return zone
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return null
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Ensure a hosted zone exists for a domain, creating it if necessary
|
|
886
|
+
* Returns the hosted zone ID suitable for use in CloudFormation
|
|
887
|
+
*/
|
|
888
|
+
async ensureHostedZone(params: {
|
|
889
|
+
domainName: string
|
|
890
|
+
comment?: string
|
|
891
|
+
}): Promise<{
|
|
892
|
+
hostedZoneId: string
|
|
893
|
+
nameServers: string[]
|
|
894
|
+
isNew: boolean
|
|
895
|
+
action: 'found' | 'created'
|
|
896
|
+
}> {
|
|
897
|
+
const result = await this.findOrCreateHostedZone({
|
|
898
|
+
domainName: params.domainName,
|
|
899
|
+
comment: params.comment,
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
// Strip /hostedzone/ prefix for compatibility
|
|
903
|
+
const hostedZoneId = result.hostedZone.Id.replace('/hostedzone/', '')
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
hostedZoneId,
|
|
907
|
+
nameServers: result.nameServers,
|
|
908
|
+
isNew: result.isNew,
|
|
909
|
+
action: result.isNew ? 'created' : 'found',
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Setup DNS for a domain with automatic hosted zone creation
|
|
915
|
+
* Creates the hosted zone if needed and returns setup information
|
|
916
|
+
*/
|
|
917
|
+
async setupDomainDns(params: {
|
|
918
|
+
domain: string
|
|
919
|
+
createIfNotExists?: boolean
|
|
920
|
+
}): Promise<{
|
|
921
|
+
success: boolean
|
|
922
|
+
hostedZoneId: string | null
|
|
923
|
+
nameServers: string[]
|
|
924
|
+
isNew: boolean
|
|
925
|
+
message: string
|
|
926
|
+
}> {
|
|
927
|
+
const { domain, createIfNotExists = true } = params
|
|
928
|
+
|
|
929
|
+
// Try to find existing zone
|
|
930
|
+
const existing = await this.findHostedZoneByName(domain)
|
|
931
|
+
|
|
932
|
+
if (existing) {
|
|
933
|
+
const zoneDetails = await this.getHostedZone({ Id: existing.Id })
|
|
934
|
+
return {
|
|
935
|
+
success: true,
|
|
936
|
+
hostedZoneId: existing.Id.replace('/hostedzone/', ''),
|
|
937
|
+
nameServers: zoneDetails.DelegationSet.NameServers,
|
|
938
|
+
isNew: false,
|
|
939
|
+
message: `Found existing hosted zone for ${domain}`,
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (!createIfNotExists) {
|
|
944
|
+
return {
|
|
945
|
+
success: false,
|
|
946
|
+
hostedZoneId: null,
|
|
947
|
+
nameServers: [],
|
|
948
|
+
isNew: false,
|
|
949
|
+
message: `No hosted zone found for ${domain} and createIfNotExists is false`,
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Create the hosted zone
|
|
954
|
+
const result = await this.createHostedZone({
|
|
955
|
+
Name: domain,
|
|
956
|
+
HostedZoneConfig: {
|
|
957
|
+
Comment: `Created automatically by ts-cloud for ${domain}`,
|
|
958
|
+
},
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
success: true,
|
|
963
|
+
hostedZoneId: result.HostedZone.Id.replace('/hostedzone/', ''),
|
|
964
|
+
nameServers: result.DelegationSet.NameServers,
|
|
965
|
+
isNew: true,
|
|
966
|
+
message: `Created new hosted zone for ${domain}. Please update your domain registrar with these name servers: ${result.DelegationSet.NameServers.join(', ')}`,
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* CloudFront hosted zone ID (global)
|
|
972
|
+
*/
|
|
973
|
+
static readonly CloudFrontHostedZoneId = 'Z2FDTNDATAQYW2'
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* S3 website hosting hosted zone IDs by region
|
|
977
|
+
*/
|
|
978
|
+
static readonly S3WebsiteHostedZoneIds: Record<string, string> = {
|
|
979
|
+
'us-east-1': 'Z3AQBSTGFYJSTF',
|
|
980
|
+
'us-east-2': 'Z2O1EMRO9K5GLX',
|
|
981
|
+
'us-west-1': 'Z2F56UZL2M1ACD',
|
|
982
|
+
'us-west-2': 'Z3BJ6K6RIION7M',
|
|
983
|
+
'ap-east-1': 'ZNB98KWMFR0R6',
|
|
984
|
+
'ap-south-1': 'Z11RGJOFQNVJUP',
|
|
985
|
+
'ap-northeast-1': 'Z2M4EHUR26P7ZW',
|
|
986
|
+
'ap-northeast-2': 'Z3W03O7B5YMIYP',
|
|
987
|
+
'ap-northeast-3': 'Z2YQB5RD63NC85',
|
|
988
|
+
'ap-southeast-1': 'Z3O0J2DXBE1FTB',
|
|
989
|
+
'ap-southeast-2': 'Z1WCIGYICN2BYD',
|
|
990
|
+
'ca-central-1': 'Z1QDHH18159H29',
|
|
991
|
+
'eu-central-1': 'Z21DNDUVLTQW6Q',
|
|
992
|
+
'eu-west-1': 'Z1BKCTXD74EZPE',
|
|
993
|
+
'eu-west-2': 'Z3GKZC51ZF0DB4',
|
|
994
|
+
'eu-west-3': 'Z3R1K369G5AVDG',
|
|
995
|
+
'eu-north-1': 'Z3BAZG2TWCNX0D',
|
|
996
|
+
'sa-east-1': 'Z7KQH4QJS55SO',
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* ALB hosted zone IDs by region
|
|
1001
|
+
*/
|
|
1002
|
+
static readonly ALBHostedZoneIds: Record<string, string> = {
|
|
1003
|
+
'us-east-1': 'Z35SXDOTRQ7X7K',
|
|
1004
|
+
'us-east-2': 'Z3AADJGX6KTTL2',
|
|
1005
|
+
'us-west-1': 'Z368ELLRRE2KJ0',
|
|
1006
|
+
'us-west-2': 'Z1H1FL5HABSF5',
|
|
1007
|
+
'ap-east-1': 'Z3DQVH9N71FHZ0',
|
|
1008
|
+
'ap-south-1': 'ZP97RAFLXTNZK',
|
|
1009
|
+
'ap-northeast-1': 'Z14GRHDCWA56QT',
|
|
1010
|
+
'ap-northeast-2': 'ZWKZPGTI48KDX',
|
|
1011
|
+
'ap-northeast-3': 'Z5LXEBD8Y73MNV',
|
|
1012
|
+
'ap-southeast-1': 'Z1LMS91P8CMLE5',
|
|
1013
|
+
'ap-southeast-2': 'Z1GM3OXH4ZPM65',
|
|
1014
|
+
'ca-central-1': 'ZQSVJUPU6J1EY',
|
|
1015
|
+
'eu-central-1': 'Z215JYRZR1TBD5',
|
|
1016
|
+
'eu-west-1': 'Z32O12XQLNTSW2',
|
|
1017
|
+
'eu-west-2': 'ZHURV8PSTC4K8',
|
|
1018
|
+
'eu-west-3': 'Z3Q77PNBQS71R4',
|
|
1019
|
+
'eu-north-1': 'Z23TAZ6LKFMNIO',
|
|
1020
|
+
'sa-east-1': 'Z2P70J7HTTTPLU',
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* API Gateway hosted zone IDs by region
|
|
1025
|
+
*/
|
|
1026
|
+
static readonly APIGatewayHostedZoneIds: Record<string, string> = {
|
|
1027
|
+
'us-east-1': 'Z1UJRXOUMOOFQ8',
|
|
1028
|
+
'us-east-2': 'ZOJJZC49E0EPZ',
|
|
1029
|
+
'us-west-1': 'Z2MUQ32089INYE',
|
|
1030
|
+
'us-west-2': 'Z2OJLYMUO9EFXC',
|
|
1031
|
+
'ap-east-1': 'Z3FD1VL90ND7K5',
|
|
1032
|
+
'ap-south-1': 'Z3VO1THU9YC4UR',
|
|
1033
|
+
'ap-northeast-1': 'Z1YSHQZHG15GKL',
|
|
1034
|
+
'ap-northeast-2': 'Z20JF4UZKIW1U8',
|
|
1035
|
+
'ap-northeast-3': 'Z2YQB5RD63NC85',
|
|
1036
|
+
'ap-southeast-1': 'ZL327KTPIQFUL',
|
|
1037
|
+
'ap-southeast-2': 'Z2RPCDW04V8134',
|
|
1038
|
+
'ca-central-1': 'Z19DQILCV0OWEC',
|
|
1039
|
+
'eu-central-1': 'Z1U9ULNL0V5AJ3',
|
|
1040
|
+
'eu-west-1': 'ZLY8HYME6SFDD',
|
|
1041
|
+
'eu-west-2': 'ZJ5UAJN8Y3Z2Q',
|
|
1042
|
+
'eu-west-3': 'Z3KY65QIEKYHQQ',
|
|
1043
|
+
'eu-north-1': 'Z3UWIKFBOOGXPP',
|
|
1044
|
+
'sa-east-1': 'ZCMLWB8V5SYIT',
|
|
1045
|
+
}
|
|
1046
|
+
}
|